pax_global_header00006660000000000000000000000064147642456040014526gustar00rootroot0000000000000052 comment=3e3e401acea46a174831a9d9a72a1017595335da deken-0.10.4/000077500000000000000000000000001476424560400126765ustar00rootroot00000000000000deken-0.10.4/LICENSE.txt000066400000000000000000000030641476424560400145240ustar00rootroot00000000000000This software is copyrighted by IOhannes m zmölnig Chris McCormick and others. The following terms (the "Standard Improved BSD License") apply to all files associated with the software unless explicitly disclaimed in individual files: Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. 3. The name of the author may not be used to endorse or promote products derived from this software without specific prior written permission. THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. deken-0.10.4/Makefile000066400000000000000000000010361476424560400143360ustar00rootroot00000000000000default: @echo "make dek: build a legacy deken-package for the deken-plugin" plugin_version := $(shell egrep "^if.*::deken::versioncheck" deken-plugin.tcl | sed -e 's| *].*||' -e 's|.* ||') dek: deken-plugin-$(plugin_version)--externals.zip .PHONY: dek deken-plugin deken-plugin: deken-plugin.tcl README.plugin.txt LICENSE.txt README.deken.pd rm -rf $@ mkdir -p $@ cp $^ $@ mv $@/README.plugin.txt $@/README.txt deken-plugin-$(plugin_version)--externals.zip: deken-plugin deken package --dekformat=0 --version=$(plugin_version) $^ deken-0.10.4/README.deken.pd000066400000000000000000000005671476424560400152550ustar00rootroot00000000000000#N canvas 88 150 489 239 12; #X text 49 42 deken: library package manager for Pd; #X text 47 53 ======================================; #X obj 49 175 cnv 15 400 30 empty empty empty 20 12 0 14 -233017 -66577 0; #X text 56 179 you MUST RESTART Pd \, in order to use the new version. ; #X text 47 77 version: v0.10.4; #X text 47 139 thank you for installing the deken-plugin.; deken-0.10.4/README.md000066400000000000000000000070351476424560400141620ustar00rootroot00000000000000A minimal package management system for Pure Data externals. ![Animated GIF demonstration of the Deken plugin user interface](https://raw.githubusercontent.com/pure-data/deken/main/deken.gif) Packages are stored on and can be installed using the `Help -> Find Packages` menu after installing the [GUI plugin](https://raw.githubusercontent.com/pure-data/deken/main/deken-plugin.tcl). ## README.1st ## Since [`Pd-0.47`](http://puredata.info/downloads/pure-data/releases/0.47-0), the `deken-plugin` is included into Pure Data itself, so the only reason to manually install it is to get the newest version. Main development of the plugin is still happening in *this* repository, so you might want to manually install the plugin to help testing new features. When manually installing the `deken-plugin`, Pd will use it if (and only if) it has a greater version number than the one included in Pd. In this case you will see something like the following in the Pd-console (you first have to raise the verbosity to `Debug`): > `[deken]: installed version [0.2.1] < 0.2.3...overwriting!` > `deken-plugin.tcl (Pd externals search) in /home/frobnozzel/.local/lib/pd/extra/deken-plugin/ loaded.` ## Download/Install ## On any recent version of Pd (that already comes with deken included), you can use `Help -> Find Packages` itself to search and install newer versions of the plugin. Just search for `deken-plugin` and install the latest and greatest release of the plugin. For manual installation (e.g. because you want to test a developer version of the plugin), click to download [deken-plugin.tcl](https://raw.githubusercontent.com/pure-data/deken/main/deken-plugin.tcl) and save it to your Pd folder: * Linux = `~/.local/lib/pd/extra/deken-plugin/` (with Pd<0.47 try `~/pd-externals/deken-plugin/`) * OSX = `~/Library/Pd/deken-plugin/` * Windows = `%AppData%\Pd\deken-plugin\` Then select `Help -> Find Packages` and type the name of the external you would like to search for. ## Trusting packages The `deken-plugin` will help you find and install Pd-libraries. However, it does not verify whether a given package is downloaded from a trusted source or not. As of now, the default package source is http://puredata.info. Anybody who has an account on that website (currently that's a few thousand people) can upload packages, that the `deken-plugin` will happily find and install for you. In order to make these packages more trustworthy, we ask people to sign their uploaded packages with the GPG-key. Unfortunately the deken-plugin does not check these signatures yet. If you are concerned about the authenticity of a given download, you can check the GPG-signature manually, by following these steps: - Navigate to `Help -> Find Packages` and search for an external - Right-Click one of the search results - Select "Copy package URL" to copy the link to the downloadable file to your clipboard - Download the package from the copied link - Back in the deken search results, select "Copy OpenGPG signature URL" - Download the GPG-signature from the copied link to the same location as the package - Run `gpg --verify` on the downloaded file If the signature is correct, you can decide yourself whether you actually trust the person who signed: - Do you trust the signature to be owned by the person? - Do you know the person? - Do you trust them enough to let them install arbitrary software on your machine? # Developers # `deken` comes with a tool to package and upload your own library builds. See [developer/README.md](./developer/README.md) for more information. deken-0.10.4/README.plugin.txt000066400000000000000000000045171476424560400157000ustar00rootroot00000000000000A minimal package management system for Pure Data externals. ============================================================ Packages are stored on and can be installed using the `Help -> Find Packages` menu. ## README.1st ## Since Pd-0.47, the `deken-plugin` is included into Pure Data itself, so the only reason to manually install it is to get the newest version. When manually installing the `deken-plugin`, Pd will use it if (and only if) it has a greater version number than the one included in Pd. In this case you will see something like the following in the Pd-console (you first have to raise the verbosity to `Debug`): > Loading plugin: /home/zmoelnig/src/puredata/deken/deken-plugin/deken-plugin.tcl > [deken]: installed version [0.2.1] < 0.2.3...overwriting! > [deken] deken-plugin.tcl (Pd externals search) loaded from /home/zmoelnig/src/puredata/deken/deken-plugin. ## Trusting packages The `deken-plugin` will help you find and install Pd-libraries. However, it does not verify whether a given package is downloaded from a trusted source or not. As of now, the default package source is https://deken.puredata.info/. Anybody who has an account on the https://puredata.info website (currently that's a few thousand people) can upload packages, that the `deken-plugin` will happily find and install for you. In order to make these packages more trustworthy, we ask people to sign their uploaded packages with the GPG-key. Unfortunately the deken-plugin does not check these signatures yet. If you are concerned about the authenticity of a given download, you can check the GPG-signature manually, by following these steps: - Navigate to `Help -> Find Packages` and search for an external - Right-Click one of the search results - Select "Copy package URL" to copy the link to the downloadable file to your clipboard - Download the package from the copied link - Back in the deken search results, select "Copy OpenGPG signature URL" - Download the GPG-signature from the copied link to the same location as the package - Run `gpg --verify` on the downloaded file If the signature is correct, you can decide yourself whether you actually trust the person who signed: - Do you trust the signature to be owned by the person? - Do you know the person? - Do you trust them enough to let them install arbitrary software on your machine? deken-0.10.4/deken-extra-plugins/000077500000000000000000000000001476424560400165645ustar00rootroot00000000000000deken-0.10.4/deken-extra-plugins/deken-xtra-apt-helper.py000077500000000000000000000141361476424560400232470ustar00rootroot00000000000000#!/usr/bin/env python3 import re import fnmatch import itertools aptcache = None class EmptyVersion: """helper class that provides an empty versions member""" def __init__(self, versions=[]): self.versions = versions def parseArgs(): import argparse parser = argparse.ArgumentParser() p = parser.add_argument_group( title="constraints", ) p.add_argument( "--os", default="Linux", choices=["Linux"], help="target operating system [DEFAULT: %(default)r]", ) p.add_argument( "--architecture", default=None, help="target CPU architecture [DEFAULT: 'native']", ) p.add_argument( "--floatsize", type=int, default=32, help="target floatsize [DEFAULT: %(default)s}]", ) p = parser.add_argument_group( title="API", ) p.add_argument( "--api", type=int, choices=[ 0, 1, ], required=True, help="Output format version (0=test; 1=current)", ) parser.add_argument( "pkg", nargs="*", help="package(s) to search for", ) args = parser.parse_args() return args def initializeAptCache(): import apt global aptcache aptcache = apt.Cache() def stripSuffix(s, suffix): if s.startswith(suffix): return s[len(suffix) :] return s def hasDependency(versioned_pkg, dependencies: list) -> bool: """returns True iff (apt.package.Version) depends on any package in (list[str]); otherwise returns False""" for d in versioned_pkg.dependencies: for bd in d: if bd.name in dependencies: return True return False def getPackages(pkgs, arch=None, floatsize=32): pd32deps = {"puredata-core", "puredata", "puredata-gui", "pd"} pd64deps = {"puredata64-core", "puredata64", "puredata-gui", "pd64"} if floatsize == 64: pddeps = pd64deps else: pddeps = pd32deps if pkgs == ["*"]: pkgs = [] pkgmatch = re.compile( "(pd-|pd64-)?(" + "|".join(fnmatch.translate(p) for p in pkgs) + ")" ).match packages = {} for p in aptcache: # filter out known non-externals if p.shortname.startswith("puredata"): continue # filter out unwanted architectures if arch and p.architecture() != arch: continue for v in p.versions: if pkgs: names = [p.shortname] + v.provides matches = [ matchedname for name in {*names} for matchedname in [pkgmatch(name)] if matchedname ] if not matches: continue else: # all externals matches = [p.shortname, None, None] if p.shortname.startswith("pd-"): matches[2] = p.shortname[3:] elif p.shortname.startswith("pd64-"): matches[2] = p.shortname[5:] matches = [matches] # we only take packages that depend on Pd if not hasDependency(v, pddeps): continue for m in matches: for n in {m[0], m[2]}: npkgs = packages.get(n) or set() npkgs.add(v) packages[n] = npkgs return itertools.chain(*packages.values()) def getOrigin( versioned_pkg, fallback_origin="apt", fallback_date=None, trusted="", untrusted="?", ): """get the (one) package origin as a tuple of repository & archive, e.g. ("Debian", "bookworm/main") if the repository is trusted, the string is appended, otherwise the . prefer trusted sources over untrusted. """ origins = [o for o in versioned_pkg.origins if o.trusted] trust = trusted if not origins: origins = versioned_pkg.origins trust = untrusted if not origins: return (fallback_origin, fallback_date, None) origin = None codename = None component = None for o in origins: if all((origin, codename, component)): break try: origin = origin or o.label or o.origin or fallback_uploader except AttributeError: pass try: codename = codename or o.codename or o.archive except AttributeError: pass try: component = component or o.component except AttributeError: pass origin = origin or fallback_origin if codename or component: if not component: codename_component = codename else: codename_component = f"{codename or '???'}/{component}" else: codename_component = fallback_date return (origin, codename_component) def showPackages(pkgs): """create a parseseable representation for each package""" for p in sorted(set(pkgs)): library = p.package.name version = p.version arch = p.architecture uploader, date = getOrigin(p) uri = p.uri status = p.summary installed = p.package.is_installed state = "Already installed" if installed else "Provided" comment = f"{state} by {uploader} ({date})" print( f"{library}\t{version}\t{arch}\t{int(installed)}\t{uploader}\t{date}\t{uri}\t{status}\t{comment}" ) def main(): args = parseArgs() if args.api not in {0, 1}: return if not args.api: # quick sanity check if python-apt is available import apt return initializeAptCache() if not args.architecture: for p in ["dpkg", "apt", "bash"]: try: args.architecture = aptcache.get(f"{p}:native").architecture() break except AttributeError: pass packages = getPackages(args.pkg, arch=args.architecture, floatsize=args.floatsize) showPackages(packages) if __name__ == "__main__": main() deken-0.10.4/deken-extra-plugins/deken-xtra-apt-plugin.tcl000066400000000000000000000230201476424560400234050ustar00rootroot00000000000000# META NAME PdExternalsSearch # META DESCRIPTION Search for externals Debian-packages via apt # META AUTHOR IOhannes m zmölnig # ex: set setl sw=2 sts=2 et # Search URL: # http://puredata.info/search_rss?SearchableText=xtrnl- # The minimum version of TCL that allows the plugin to run package require Tcl 8.4 ## #################################################################### ## searching apt (if available) namespace eval ::deken::apt { namespace export search namespace export install variable distribution variable pluginpath $::current_plugin_loadpath } proc ::deken::apt::search {name} { if { [ info proc ${::deken::apt::searcher} ] } { return [::deken::apt::search_pyapt ${name}] } return } proc ::deken::apt::search_pyapt {name} { set result {} set cmd "${::deken::apt::search_pyaptscript} --api 1 --os $::deken::platform(os) --architecture $::deken::platform(machine) --floatsize $::deken::platform(floatsize) -- ${name}" set io [open "|${cmd}" ] while { [gets ${io} line ] >= 0 } { foreach {pkgname version arch is_installed uploader date uri status comment} [ split "${line}" "\t" ] {break} set name ${pkgname} set cmd [list ::deken::apt::install ${pkgname}=${version}] set match 1 set contextcmds {} if { ${uri} ne {} } { lappend contextcmds [list [_ "Open package webpage" ] "pd_menucommands::menu_openfile [file dirname ${uri}]"] lappend contextcmds [list [_ "Copy package URL" ] "clipboard clear; clipboard append ${uri}"] if { ${is_installed} } { lappend contextcmds {} } } if { ${is_installed} } { lappend contextcmds [::deken::apt::contextmenu::uninstall ${pkgname}] } set contextcmd [list ::deken::apt::contextmenu %W %x %y $contextcmds] set norm [::deken::normalize_result "${pkgname} - ${status}" ${cmd} ${match} ${comment} ${status} ${contextcmd} ${pkgname} ${version} ${uploader} ${date}] lappend result ${norm} } close ${io} return ${result} } proc ::deken::apt::search_madison {name} { set result [] if { [info exists ::deken::apt::distribution] } { } { if { [ catch { exec lsb_release -si } ::deken::apt::distribution ] } { set ::deken::apt::distribution {} } } if { "${::deken::apt::distribution}" == "" } { return } set name [ string tolower ${name} ] array unset pkgs array set pkgs {} set _dpkg_query {dpkg-query -W -f ${db:Status-Abbrev}${Version}\n} # pd-externals must depend on Pd somehow # (this misses packages that provide pd-externals among *other* things, # and therefore would only 'Recommend' Pd...) set pdpkgs {pd puredata puredata-core puredata-gui} if { $::deken::platform(floatsize) == 64 } { set pdpkgs {pd64 puredata64 puredata64-core puredata-gui} } set pdfilter [concat {-F Depends -w} [join $pdpkgs " --or -F Depends -w "]] set filter ${pdfilter} if { "${name}" == "" } { } { set filter "-F Package ${name} --and ( ${filter} )" } #set io [ open "|grep-aptavail -n -s Package,Version ${filter} | paste -sort -u | xargs apt-cache madison" r ] set io [ open "|grep-aptavail -n -s Package ${filter} | sort -u | xargs apt-cache madison" r ] while { [ gets ${io} line ] >= 0 } { set llin [ split "${line}" "|" ] set pkgname [ string trim [ lindex ${llin} 0 ] ] #if { ${pkgname} ne ${searchname} } { continue } set ver_ [ string trim [ lindex ${llin} 1 ] ] set info_ [ string trim [ lindex ${llin} 2 ] ] ## status: is the package installed? set state "Provided" set installed 0 catch { set io2cmd "|${_dpkg_query} ${pkgname} | grep -w -F \"ii ${ver_}\"" set io2 [ open "${io2cmd}" ] if { [ gets ${io2} _ ] >= 0 } { set state "Already installed" set installed 1 } { while { [ gets ${io2} _ ] >= 0 } { } } } if { "Packages" eq [ lindex ${info_} end ] } { set suite [ lindex ${info_} 1 ] set arch [ lindex ${info_} 2 ] if { ! [ info exists pkgs(${pkgname}/${ver_}) ] } { set pkgs(${pkgname}/${ver_}) [ list ${pkgname} ${ver_} ${suite} ${arch} ${state} ${installed}] } } } foreach {name inf} [ array get pkgs ] { set pkgname [ lindex ${inf} 0 ] set v [ lindex ${inf} 1 ] set suite [ lindex ${inf} 2 ] set arch [ lindex ${inf} 3 ] set state [ lindex ${inf} 4 ] set cmd [list ::deken::apt::install ${pkgname}=${v}] set match 1 set comment "${state} by ${::deken::apt::distribution} (${suite})" set status "${pkgname}_${v}_${arch}.deb" set contextcmd {} set contextcmds {} if { [ lindex ${inf} 5 ] } { lappend contextcmds [::deken::apt::contextmenu::uninstall ${pkgname}] } if { ${contextcmds} eq {} } { set contextcmd {} } else { set contextcmd [list ::deken::apt::contextmenu %W %x %y $contextcmds] } lappend result [list ${name} ${cmd} ${match} ${comment} ${status} ${pkgname} ${v} ${suite} ${contextcmd}] } # version-sort the results and normalize the result-string set sortedresult [] if {[llength [info procs ::deken::normalize_result ]] > 0} { foreach r [lsort -dictionary -decreasing -index 1 ${result} ] { foreach {title cmd match comment status pkgname version suite cmd2} ${r} {break} lappend sortedresult [::deken::normalize_result ${title} ${cmd} ${match} ${comment} ${status} ${cmd2} ${pkgname} ${version} Debian ${suite}] } } { foreach r [lsort -dictionary -decreasing -index 1 ${result} ] { # [list ${title} ${cmd} ${match} ${comment} ${status}] foreach {title cmd match comment status} ${r} {break} lappend sortedresult [list ${title} ${cmd} ${match} ${comment} ${status}] } } return ${sortedresult} } proc ::deken::apt::contextmenu {widget theX theY commands} { set m .dekenresults_contextMenu destroy ${m} if { ${commands} eq {} } { return } menu ${m} foreach lblcmd ${commands} { if { ${lblcmd} eq {} } { ${m} add separator } else { foreach {lbl cmd} ${lblcmd} {break} ${m} add command -label ${lbl} -command ${cmd} } } tk_popup ${m} [expr {[winfo rootx ${widget}] + ${theX}}] [expr {[winfo rooty ${widget}] + ${theY}}] } namespace eval ::deken::apt::contextmenu:: {} proc ::deken::apt::contextmenu::uninstall {pkgname} { return [list [format [_ "Uninstall '%s'" ] ${pkgname}] [list ::deken::apt::uninstall ${pkgname}]] } proc ::deken::apt::getsudo {} { # for whatever reasons, we cannot have 'deken' as the description # (it will always show ${prog} instead) set desc deken::apt if { [ catch { exec which pkexec } sudo ] } { if { [ catch { exec which gksudo } sudo ] } { set sudo "" } { set sudo "${sudo} -D ${desc} --" } } if { ${sudo} == "" } { ::deken::post "Please install 'policykit-1', if you want to install system packages via deken..." error } return ${sudo} } proc ::deken::apt::install {pkg {version {}}} { if { ${version} ne {} } { set pkg "${pkg}=${version}" } set prog "apt-get install -y --show-progress ${pkg}" set sudo [::deken::apt::getsudo] if { ${sudo} ne "" } { set cmdline "${sudo} ${prog}" #::deken::post "${cmdline}" error set io [ open "|${cmdline}" ] while { [ gets ${io} line ] >= 0 } { ::deken::post "apt: ${line}" } if { [ catch { close ${io} } ret ] } { ::deken::post "apt::install failed to install ${pkg}" error ::deken::post "\tDid you provide the correct password and/or" error ::deken::post "\tis the apt database locked by another process?" error } } } proc ::deken::apt::uninstall {pkg} { set prog "apt-get remove -y --show-progress ${pkg}" set sudo [::deken::apt::getsudo] if { ${sudo} ne "" } { set cmdline "${sudo} ${prog}" #::deken::post "${cmdline}" error set io [ open "|${cmdline}" ] while { [ gets ${io} line ] >= 0 } { ::deken::post "apt: ${line}" } if { [ catch { close ${io} } ret ] } { ::deken::post "apt::uninstall failed to remove ${pkg}" error ::deken::post "\tDid you provide the correct password and/or" error ::deken::post "\tis the apt database locked by another process?" error } } } proc ::deken::apt::register { } { set pyfile [file join ${::deken::apt::pluginpath} deken-xtra-apt-helper.py] if { [ catch { exec ${pyfile} --api 0 set ::deken::apt::search_pyaptscript ${pyfile} ::deken::register ::deken::apt::search_pyapt } ] } { # failed to initialize python-apt backend } else { return 1 } if { [ catch { exec apt-cache madison } _ ] } { } { if { [ catch { exec which grep-aptavail } _ ] } { } { if { [ catch { ::deken::register ::deken::apt::search_madison } ] } { # } else { return 1 } }} ::pdwindow::debug "Not using unavailable APT-backend for deken\n" return 0 } proc ::deken::apt::initialize { } { if { [::deken::apt::register] } { ::pdwindow::debug "Using APT as additional deken backend\n" } } after idle ::deken::apt::initialize deken-0.10.4/deken-plugin.tcl000066400000000000000000004150731476424560400157760ustar00rootroot00000000000000# META NAME PdExternalsSearch # META DESCRIPTION Search for externals zipfiles on puredata.info # META AUTHOR chris@mccormick.cx # META AUTHOR zmoelnig@iem.at # ex: set setl sw=2 sts=2 et # Search URL: # http://deken.puredata.info/search?name=foobar # TODOs ## + open embedded README ## - open README on homepage (aka "More info...") ## + remove library before unzipping ## + only show valid arch ## - only show most recent version (of each arch) ## - check whether the "cd" thing during unzip works on w32 and multiple drives ## - redirect ::deken::post to ::pdwindow::post (that is: use the results pane only for results) ## + make the "add to path" thingy configurable # The minimum version of TCL that allows the plugin to run package require Tcl 8.4 9 # If Tk or Ttk is needed #package require Ttk # Any elements of the Pd GUI that are required # + require everything and all your script needs. # If a requirement is missing, # Pd will load, but the script will not. package require http 2 # try enabling https if possible if { [catch {package require tls} ] } {} else { ::tls::init -ssl2 false -ssl3 false -tls1 true ::http::register https 443 ::tls::socket } # try enabling PROXY support if possible if { [catch {package require autoproxy} ] } {} else { ::autoproxy::init if { ! [catch {package present tls} stdout] } { ::http::register https 443 ::autoproxy::tls_socket } } package require pdwindow 0.1 package require pd_menucommands 0.1 package require pd_guiprefs package require scrollboxwindow package require scrollbox namespace eval ::deken:: { variable version variable installpath variable userplatform variable hideforeignarch variable hideoldversions # results: {{title} {cmd} {description} {url} {ctxmenu}} variable results # selected: {library} {cmd} ... variable selected {} } namespace eval ::deken::preferences { variable installpath variable installpath_x variable userinstallpath # automatically detected platform variable platform # user specified platform variable userplatform # boolean whether non-matching archs should be hidden variable hideforeignarch variable hideoldversions } namespace eval ::deken::utilities { } ## only register this plugin if there isn't any newer version already registered ## (if ::deken::version is defined and is higher than our own version) proc ::deken::versioncheck {version} { if { [info exists ::deken::version ] } { set v0 [split ${::deken::version} "."] set v1 [split ${version} "."] foreach x ${v0} y ${v1} { if { ${x} > ${y} } { set msg [format [_ "\[deken\] installed version \[%1\$s\] > %2\$s...skipping!" ] ${::deken::version} ${version} ] ::pdwindow::debug "${msg}\n" return 0 } if { ${x} < ${y} } { set msg [format [_ "\[deken\] installed version \[%1\$s\] < %2\$s...overwriting!" ] ${::deken::version} ${version} ] ::pdwindow::debug "${msg}\n" set ::deken::version ${version} return 1 } } set msg [format [_ "\[deken\] installed version \[%1\$s\] == %2\$s...skipping!" ] ${::deken::version} ${version} ] ::pdwindow::debug "${msg}\n" return 0 } set ::deken::version ${version} return 1 } ## put the current version of this package here: if { [::deken::versioncheck 0.10.4] } { namespace eval ::deken:: { namespace export open_searchui variable winid .externals_searchui variable resultsid ${winid}.results variable infoid ${winid}.results variable platform variable architecture_substitutes variable installpath variable statustext variable statustimer variable backends variable progressvar variable progresstext variable progresstimer namespace export register } namespace eval ::deken::search:: { } namespace eval ::deken::search::dekenserver { } ## FIXXXXME only initialize vars if not yet set set ::deken::backends {} set ::deken::installpath {} set ::deken::userplatform {} set ::deken::hideforeignarch false set ::deken::hideoldversions false set ::deken::show_readme 1 set ::deken::remove_on_install 1 set ::deken::add_to_path 0 set ::deken::keep_package 0 set ::deken::verify_sha256 1 set ::deken::searchtype name set ::deken::statustimer {} set ::deken::progresstimer {} set ::deken::preferences::installpath {} set ::deken::preferences::installpath_x {} set ::deken::preferences::userinstallpath {} set ::deken::preferences::platform {} set ::deken::preferences::userplatform {} set ::deken::preferences::hideforeignarch {} set ::deken::preferences::hideoldversions {} set ::deken::preferences::show_readme {} set ::deken::preferences::remove_on_install {} set ::deken::preferences::add_to_path {} set ::deken::preferences::add_to_path_temp {} set ::deken::preferences::keep_package {} set ::deken::preferences::verify_sha256 {} set ::deken::preferences::use_url_primary 1 set ::deken::preferences::use_urls_secondary 0 set ::deken::preferences::urls_secondary {} set ::deken::preferences::use_urls_ephemeral 0 set ::deken::preferences::urls_ephemeral {} set ::deken::platform(os) ${::tcl_platform(os)} set ::deken::platform(machine) [string tolower ${::tcl_platform(machine)}] set ::deken::platform(bits) [ expr {[ string length [ format %X -1 ] ] * 4} ] set ::deken::platform(floatsize) 32 # architectures that can be substituted for each other array set ::deken::architecture_substitutes {} set ::deken::architecture_substitutes(x86_64) [list "amd64" ] set ::deken::architecture_substitutes(amd64) [list "x86_64" ] set ::deken::architecture_substitutes(i686) [list "i586" "i386"] set ::deken::architecture_substitutes(i586) [list "i386"] set ::deken::architecture_substitutes(armv6) [list "armv6l" "arm"] set ::deken::architecture_substitutes(armv6l) [list "armv6" "arm"] set ::deken::architecture_substitutes(armv7) [list "armv7l" "armv6l" "armv6" "arm"] set ::deken::architecture_substitutes(armv7l) [list "armv7" "armv6l" "armv6" "arm"] set ::deken::architecture_substitutes(powerpc) [list "ppc"] set ::deken::architecture_substitutes(ppc) [list "powerpc"] set ::deken::architecture_normalize(x86_64) "amd64" set ::deken::architecture_normalize(i686) "i386" set ::deken::architecture_normalize(i586) "i386" set ::deken::architecture_normalize(i486) "i386" set ::deken::architecture_normalize(armv6l) "armv6" set ::deken::architecture_normalize(armv7l) "armv7" set ::deken::architecture_normalize(powerpc) "ppc" # normalize W32 OSs if { [ string match "Windows *" "${::deken::platform(os)}" ] > 0 } { # we are not interested in the w32 flavour, so we just use 'Windows' for all of them set ::deken::platform(os) "Windows" } # normalize W32 CPUs if { "Windows" eq "${::deken::platform(os)}" } { # in redmond, intel only produces 32bit CPUs,... if { "intel" eq "${::deken::platform(machine)}" } { set ::deken::platform(machine) "i686" } # ... and all 64bit CPUs are manufactured by amd #if { "amd64" eq "$::deken::platform(machine)" } { set ::deken::platform(machine) "x86_64" } } catch { set ::deken::platform(machine) $::deken::architecture_normalize($::deken::platform(machine)) } # ###################################################################### # ################ compatibility ####################################### # ###################################################################### # list-reverter (compat for tcl<8.5) if {[info commands lreverse] == ""} { proc lreverse list { set res {} set i [llength ${list}] while {${i}} { lappend res [lindex ${list} [incr i -1]] } set res } ;# RS } # ###################################################################### # ################ utilities ########################################## # ###################################################################### proc ::deken::utilities::setdefault {key value} { upvar ${key} k if { [info exists k] } { set k } else { set k ${value} } } proc ::deken::utilities::bool {value {fallback 0}} { catch {set fallback [expr {bool(${value})} ] } stdout return ${fallback} } proc ::deken::utilities::int {value {fallback 0}} { catch {set fallback [expr {int(${value})} ] } stdout return ${fallback} } proc ::deken::utilities::tristate {value {offset 0} {fallback 0} } { catch {set fallback [expr {(int(${value}) + int(${offset}))% 3} ]} stdout return ${fallback} } proc ::deken::utilities::list_unique {lst} { array set cache {} set result {} foreach element ${lst} { if { ! [info exists cache(${element})]} { lappend result ${element} } set cache(${element}) 1 } return ${result} } proc ::deken::utilities::lists_intersect {args} { set numlists [llength ${args}] if {${numlists} < 1} {return {}} set cache("") 0 set elements {} foreach lst ${args} { set lst [::deken::utilities::list_unique ${lst}] foreach element ${lst} { if { ! [info exists cache(${element})]} { lappend elements ${element} } incr cache(${element}) } } # ${elements} holds a list of unique elements (as they appeared) # so filter out those that were not in all lists set cache("") 0 set result {} foreach element ${elements} { if { ${numlists} == $cache(${element}) } { lappend result ${element} } } return ${result} } proc ::deken::utilities::expandpath {path} { set map "@PD_PATH@" lappend map ${::sys_libdir} string map ${map} ${path} } proc ::deken::utilities::get_tmpfilename {{path ""} {ext ""} {prefix dekentmp}} { for {set i 0} {true} {incr i} { set tmpfile [file join ${path} ${prefix}.${i}${ext}] if {![file exists ${tmpfile}]} { return ${tmpfile} } } } proc ::deken::utilities::get_tmpdir {} { proc _iswdir {d} { return [expr {[file isdirectory ${d}] * [file writable ${d}]}] } set tmpdir "" # TRASH_FOLDER: very old Macintosh. Mac OS X doesn't have this. # TMPDIR: unices # TMP, TEMP: windows # TEPMDIR: for symmetry :-) foreach {d} {TRASH_FOLDER TMPDIR TEMPDIR TEMP TMP} { if { [info exists ::env(${d}) ] } { set tmpdir $::env(${d}) if {[_iswdir ${tmpdir}]} {return ${tmpdir}} } } set tmpdir "/tmp" if {[_iswdir ${tmpdir}]} {return ${tmpdir}} set tmpdir [pwd] if {[_iswdir ${tmpdir}]} {return ${tmpdir}} } proc ::deken::utilities::is_writabledir {path} { set fs [file separator] set access [list RDWR CREAT EXCL TRUNC] set tmpfile [::deken::utilities::get_tmpfilename ${path}] # try creating tmpfile if {![catch {open ${tmpfile} ${access}} channel]} { close ${channel} file delete ${tmpfile} return true } return false } proc ::deken::utilities::get_writabledir {paths} { foreach p ${paths} { set xp [ ::deken::utilities::expandpath ${p} ] if { [ ::deken::utilities::is_writabledir ${xp} ] } { return ${p} } } return } proc ::deken::utilities::rmrecursive {path} { # recursively remove ${path} if it exists, traversing into each directory # to delete single items (rather than just unlinking the parent directory) set errors 0 set myname [lindex [info level 0] 0] set children [glob -nocomplain -directory ${path} -types hidden *] lappend children {*}[glob -nocomplain -directory ${path} *] foreach child $children[set children {}] { if {[file tail ${child}] in {. ..}} { continue } if {[file isdirectory ${child}]} { if {[file type ${child}] ne "link"} { incr errors [${myname} ${child}] } } if { [ catch { file delete -force ${child} } ] } { incr errors } } return ${errors} } # http://rosettacode.org/wiki/URL_decoding#Tcl proc ::deken::utilities::urldecode {str} { set specialMap {"[" "%5B" "]" "%5D"} set seqRE {%([0-9a-fA-F]{2})} set replacement {[format "%c" [scan "\1" "%2x"]]} set modStr [regsub -all ${seqRE} [string map ${specialMap} ${str}] ${replacement}] encoding convertfrom utf-8 [subst -nobackslashes -novariables ${modStr}] } proc ::deken::utilities::verbose {level message} { ::pdwindow::verbose ${level} "\[deken\] ${message}\n" } proc ::deken::utilities::debug {message} { set winid ${::deken::winid} if {[winfo exists ${winid}.tab.info]} { ::deken::post ${message} debug } else { ::pdwindow::debug "\[deken\] ${message}\n" } } if { [catch {package require tkdnd} ] } { proc ::deken::utilities::dnd_init {windowid} { } } else { proc ::deken::utilities::dnd_init {windowid} { ::tkdnd::drop_target register ${windowid} DND_Files bind ${windowid} <> {::deken::utilities::dnd_drop_files %D} } proc ::deken::utilities::dnd_drop_files {files} { foreach f ${files} { if { [regexp -all -nocase "\.(zip|dek|tgz|tar\.gz)$" ${f} ] } { set msg [format [_ "installing deken package '%s'" ] ${f}] ::deken::statuspost ${msg} ::deken::install_package_from_file ${f} } else { set msg [format [_ "ignoring '%s': doesn't look like a deken package" ] ${f}] ::deken::statuspost ${msg} } } return "link" } } namespace eval ::deken::utilities::unzipper:: { # we put a number of unzip methods in this namespace. # they are tried in in alphabetical(!) order # zipfs from Tcl>=8.7 (it seems this only works with Tcl>=9.0) catch { zipfs root proc builtin_zipfs {zipfile path} { if { [catch { set base [file join [zipfs root] deken] if { [catch { package require Tcl 8 # yikes! Tcl8.7 uses 'zipfs mount ' zipfs mount ${base} ${zipfile} } ] } { # and Tcl9 uses 'zipfs mount ' zipfs mount ${zipfile} ${base} } foreach x [glob [file join ${base} *]] { file copy -force -- ${x} ${path} } zipfs unmount ${base} } stdout ] } { ::deken::utilities::debug "unzipper::zipfs: ${stdout}" return 0 } return 1 } } # ::zipfile::decode from tcllib catch { package require zipfile::decode proc tcllib_zipfile {zipfile path} { if { [catch { ::zipfile::decode::unzipfile "${zipfile}" "${path}" ::zipfile::decode::open "${zipfile}" set zdict [::zipfile::decode::archive] ::zipfile::decode::close foreach zfile [::zipfile::decode::files ${zdict}] { set zfile [file join ${path} ${zfile}] catch { if { ! [file attributes ${zfile} -permissions] } { if [file isdirectory ${zfile} ] { set perms 0700 } else { set perms 0600 } file attributes ${zfile} -permissions ${perms} } } } } stdout ] } { ::deken::utilities::debug "unzipper::zipfile::decode: ${stdout}" return 0 } return 1 } } proc untar {zipfile path} { if { [catch { exec tar -xf "${zipfile}" -C "${path}" } stdout ] } { ::deken::utilities::debug "unzipper::tar ${stdout}" return 0 } return 1 } proc unzip {zipfile path} { set result 0 if { [catch { exec unzip -uo "${zipfile}" -d "${path}" set result 1 } stdout ] } { ::deken::utilities::debug "unzipper::unzip ${stdout}" } return ${result} } if {$::tcl_platform(platform) eq "windows"} { ## VisualBasic is w32 only proc windows_visualbasic {zipfile path} { ## create script-file set vbsscript [::deken::utilities::get_tmpfilename [::deken::utilities::get_tmpdir] ".vbs" ] set script { On Error Resume Next Set fso = CreateObject("Scripting.FileSystemObject") 'The location of the zip file. ZipFile = fso.GetAbsolutePathName(WScript.Arguments.Item(0)) 'The folder the contents should be extracted to. ExtractTo = fso.GetAbsolutePathName(WScript.Arguments.Item(1)) 'If the extraction location does not exist create it. If NOT fso.FolderExists(ExtractTo) Then fso.CreateFolder(ExtractTo) End If 'Extract the contents of the zip file. set objShell = CreateObject("Shell.Application") set FilesInZip=objShell.NameSpace(ZipFile).items objShell.NameSpace(ExtractTo).CopyHere(FilesInZip) 'In case of an error, exit If Err.Number <> 0 Then Err.Clear WScript.Quit 1 End If Set fso = Nothing Set objShell = Nothing } if {![catch {set fileId [open ${vbsscript} "w"]}]} { puts ${fileId} ${script} close ${fileId} } if {![file exists ${vbsscript}]} { ## still no script, give up return 0 } ## try to call the script ## (and windows requires the file to have a .zip extension!!!) if { [ catch { set zipfilezip ${zipfile}.zip file rename ${zipfile} ${zipfilezip} exec cscript "${vbsscript}" "${zipfilezip}" . file rename ${zipfilezip} ${zipfile} } stdout ] } { catch { file rename ${zipfilezip} ${zipfile} } catch { file delete "${vbsscript}" } ::deken::utilities::debug "unzipper::VBS-unzip(${vbsscript}): ${stdout}" return 0 } catch { file delete "${vbsscript}" } return 1 } } } proc ::deken::utilities::unzipper {zipfile {path .}} { set unzippers [lsort -dictionary [info procs ::deken::utilities::unzipper::*]] foreach unzip ${unzippers} { set result 0 catch { set result [ ${unzip} ${zipfile} ${path} ] } if {${result} ne 0} { return 1 } } return 0 } proc ::deken::utilities::extract {installdir filename fullpkgfile {keep_package 1}} { if { ! [ file isdirectory "${installdir}" ] } { return 0 } ::deken::statuspost [format [_ "Installing '%s'" ] ${filename} ] debug set PWD [ pwd ] cd ${installdir} set success 1 if { [ string match *.dek ${fullpkgfile} ] } then { if { ! [ ::deken::utilities::unzipper ${fullpkgfile} ${installdir} ] } { if { [ catch { exec unzip -uo ${fullpkgfile} } stdout ] } { ::deken::utilities::debug "${stdout}" set success 0 } } } elseif { [ string match *.zip ${fullpkgfile} ] } then { if { ! [ ::deken::utilities::unzipper ${fullpkgfile} ${installdir} ] } { if { [ catch { exec unzip -uo ${fullpkgfile} } stdout ] } { ::deken::utilities::debug "${stdout}" set success 0 } } } elseif { [ string match *.tar.* ${fullpkgfile} ] || [ string match *.tgz ${fullpkgfile} ] } then { if { [ catch { exec tar xf ${fullpkgfile} } stdout ] } { ::deken::utilities::debug "${stdout}" set success 0 } } cd ${PWD} if { ${success} > 0 } { ::deken::post [format [_ "Successfully unzipped %1\$s into %2\$s."] ${filename} ${installdir} ] debug if { ! "${keep_package}" } { catch { file delete ${fullpkgfile} } } } else { # Open both the fullpkgfile folder and the zipfile itself # NOTE: in tcl 8.6 it should be possible to use the zlib interface to actually do the unzip set msg [_ "Unable to extract package automatically." ] ::deken::post "${msg}" warn ::pdwindow::error "${msg}\n" set msg "" append msg [_ "Please perform the following steps manually:" ] append msg "\n" append msg [format [_ "1. Unzip %s." ] ${fullpkgfile} ] append msg "\n" if { [string match "*.dek" ${fullpkgfile}] } { append msg " " append msg [_ "You might need to change the file-extension from .dek to .zip" ] append msg "\n" } append msg [format [_ "2. Copy the contents into %s." ] ${installdir}] append msg "\n" append msg [format [_ "3. Remove %s. (optional)" ] ${fullpkgfile} ] append msg "\n" ::deken::post "${msg}" pd_menucommands::menu_openfile ${fullpkgfile} pd_menucommands::menu_openfile ${installdir} } return ${success} } proc ::deken::utilities::uninstall {path library} { # recursively remove ${path}/${library} if it exists set fullpath [file join ${path} ${library}] if {[file exists ${fullpath}]} { ::deken::post [format [_ "Removing '%s'" ] ${fullpath} ] debug if { [catch { file delete -force "${fullpath}" } stdout ] } { set msg [format [_ "Uninstalling %1\$s from %2\$s failed!"] ${library} ${path}] ::deken::utilities::debug "${msg}\n ${stdout}" return 0 } } return 1 } namespace eval ::deken::utilities::sha256:: { # we put a number of sha256sum methods in this namespace # they are tried in alphabetical order proc sha256sum {filename} { set hash {} catch { set hash [lindex [exec sha256sum ${filename}] 0] } return ${hash} } proc shasum {filename} { set hash {} catch { set hash [lindex [exec shasum -a 256 ${filename}] 0] } return ${hash} } if {$::tcl_platform(platform) eq "windows"} { proc winpowershell {filename} { set batscript [::deken::utilities::get_tmpfilename [::deken::utilities::get_tmpdir] ".bat" ] set script { @echo off powershell -Command " & {Get-FileHash -Algorithm SHA256 -LiteralPath \"%1\" | Select-Object -ExpandProperty Hash}" } if {![catch {set fileId [open ${batscript} "w"]}]} { puts ${fileId} ${script} close ${fileId} } if {![file exists ${batscript}]} { ## still no script, give up return "" } if { [ catch { set hash [exec "${batscript}" "${filename}"] } stdout ] } { # ouch, couldn't run powershell script ::deken::utilities::verbose 1 "sha256.ps1 error: ${stdout}" set hash "" } catch { file delete "${batscript}" } return ${hash} } proc windows {filename} { set hash {} catch { regexp {([a-fA-F0-9]{64})} [exec certUtil -hashfile ${filename} SHA256] hash } return ${hash} } } catch { package require sha256 proc ZZZtcllib {filename} { # the tcllib implementation comes last, as it is really slow... set hash {} catch { set hash [::sha2::sha256 -hex -filename ${filename}] } return ${hash} } } } proc ::deken::utilities::verify_sha256 {url pkgfile} { set msg [format [_ "Skipping SHA256 verification of '%s'." ] ${url} ] ::deken::statuspost ${msg} return -100 } foreach impl [lsort -dictionary [info procs ::deken::utilities::sha256::*]] { # skip any $impl that does not return a valid sha256 (or even throws an error) if { [catch { if { [${impl} ${::argv0}] eq "" } { continue } }] } { continue } # short name for the actually used interpreter interp alias {} ::deken::utilities::sha256 {} ${impl} proc ::deken::utilities::verify_sha256 {url pkgfile} { ::deken::statuspost [format [_ "SHA256 verification of '%s'" ] ${pkgfile} ] debug ::deken::syncgui set retval 1 set isremote 1 set hashfile "" # check if ${url} really is a local file if { [file normalize ${url}] eq ${url} } { # ${url} is really an absolute filename # use it, if it exists set hashfile "${url}.sha256" set isremote 0 if { [file isfile ${hashfile} ] && [file readable ${hashfile}] } { } else { set msg [format [_ "Unable to fetch reference SHA256 for '%s'." ] ${url} ] ::deken::utilities::verbose 0 ${msg} ::deken::statuspost ${msg} warn 0 return -10 } } else { # otherwise fetch it from the internet if { [ catch { set hashfile [::deken::utilities::download_file ${url}.sha256 [::deken::utilities::get_tmpfilename [::deken::utilities::get_tmpdir] ".sha256" ] ] } stdout ] } { ::deken::utilities::verbose 0 "${stdout}" # unable to download set msg [format [_ "Unable to fetch reference SHA256 for '%s'." ] ${url} ] ::deken::utilities::verbose 0 ${msg} ::deken::statuspost ${msg} warn 0 return -10 } } if { "${hashfile}" eq "" } { set retval -10 } if { [ catch { set fp [open ${hashfile} r] set reference [string trim [string tolower [read ${fp}] ] ] close ${fp} if { ${isremote} } { catch { file delete ${hashfile} } } # get hash of file set hash [::deken::utilities::sha256 ${pkgfile} ] set hash [string trim [string tolower ${hash} ] ] if { ${hash} == "" } { set msg [format [_ "Skipping SHA256 verification of '%s'." ] ${url} ] ::deken::statuspost ${msg} return -100 } # check if hash is sane if { [string length ${hash}] != 64 || ! [string is xdigit ${hash}] } { ::deken::statuspost [format [_ "File checksum looks invalid: '%s'." ] ${hash}] warn 0 } # check if reference is sane if { [string length ${reference}] != 64 || ! [string is xdigit ${reference}] } { # this is more grave than the sanity check for the file hash # (since for the file hash we depend on the user's machine being able to # produce a proper SHA256 hash) ::deken::statuspost [format [_ "Reference checksum looks invalid: '%s'." ] ${reference}] error 0 } if { [string first ${reference} ${hash}] >= 0 } { set retval 1 } else { # SHA256 verification failed... set retval 0 } } stdout ] } { ::deken::utilities::verbose 0 "${stdout}" # unable to verify set msg [format [_ "Unable to perform SHA256 verification for '%s'." ] ${url} ] ::deken::utilities::verbose 0 ${msg} ::deken::statuspost ${msg} warn 0 set retval -20 } return ${retval} } # it seems we found a working sha256 implementation, don't try the other ones... break } proc ::deken::utilities::httpuseragent {} { set httpagent [::http::config -useragent] set pdversion "Pd/${::PD_MAJOR_VERSION}.${::PD_MINOR_VERSION}.${::PD_BUGFIX_VERSION}${::PD_TEST_VERSION}" set platformstring [::deken::platform2string] set tclversion "Tcl/[info patchlevel]" ::http::config -useragent "Deken/${::deken::version} (${platformstring}) ${pdversion} ${tclversion}" return ${httpagent} } # download a file to a location # http://wiki.tcl.tk/15303 proc ::deken::utilities::download_file {url outputfilename {progressproc {}}} { set URL [string map {{[} "%5b" {]} "%5d"} ${url}] set downloadfilename [::deken::utilities::get_tmpfilename [file dirname ${outputfilename}] ] set f [open ${downloadfilename} w] fconfigure ${f} -translation binary set httpagent [::deken::utilities::httpuseragent] if { [catch { if { ${progressproc} eq {} } { set httpresult [::http::geturl ${URL} -binary true -channel ${f}] } else { set httpresult [::http::geturl ${URL} -binary true -progress ${progressproc} -channel ${f}] } set ncode [::http::ncode ${httpresult}] if {${ncode} != 200} { ## FIXXME: we probably should handle redirects correctly (following them...) set err [::http::code ${httpresult}] set msg [format [_ "Unable to download from %1\$s \[%2\$s\]" ] ${url} ${err} ] ::deken::post "${msg}" debug set outputfilename "" } ::http::cleanup ${httpresult} } stdout ] } { set msg [format [_ "Unable to download from '%s'!" ] ${url} ] tk_messageBox \ -title [_ "Download failed" ] \ -message "${msg}\n${stdout}" \ -icon error -type ok \ -parent ${::deken::winid} set outputfilename "" } ::http::config -useragent ${httpagent} flush ${f} close ${f} if { "${outputfilename}" != "" } { catch { file delete ${outputfilename} } if {[file exists ${outputfilename}]} { ::deken::utilities::debug [format [_ "Unable to remove stray file '%s'" ] ${outputfilename} ] set outputfilename "" } } if { ${outputfilename} != "" && "${outputfilename}" != "${downloadfilename}" } { if {[catch { file rename ${downloadfilename} ${outputfilename}}]} { ::deken::utilities::debug [format [_ "Unable to rename downloaded file to '%s'" ] ${outputfilename} ] set outputfilename "" } } if { "${outputfilename}" eq "" } { file delete ${downloadfilename} } return ${outputfilename} } # parse a deken-packagefilename into it's components: # v0:: [-v-]?{()}-externals. # v1:: [\[)} # return: list [list ...] proc ::deken::utilities::parse_filename {filename} { set pkgname ${filename} set archs [list] set version "" if { [ string match "*.dek" ${filename} ] } { ## deken filename v1: [v]()().dek set archstring "" if { ! [regexp {^([^\[\]\(\)]+)((\[[^\[\]\(\)]+\])*)((\([^\[\]\(\)]+\))*).*\.dek$} ${filename} _ pkgname optionstring _ archstring] } { # oops, somewhat unparseable (e.g. a copy) } else { foreach {o _} [lreplace [split ${optionstring} {[]}] 0 0] { if {![string first v ${o}]} { set version [string range ${o} 1 end] } else { # ignoring unknown option... return [list {} {} {}] } } foreach {a _} [lreplace [split ${archstring} "()"] 0 0] { lappend archs ${a} } } } elseif { [ regexp {(.*)-externals\..*} ${filename} _ basename] } { ## deken filename v0 set pkgname ${basename} # basename [-v-]?{()} ## strip off the archs set baselist [split ${basename} () ] # get pkgname + version set pkgver [lindex ${baselist} 0] if { ! [ regexp "(.*)-v(.*)-" ${pkgver} _ pkgname version ] } { set pkgname ${pkgver} set version "" } # get archs foreach {a _} [lreplace ${baselist} 0 0] { # in filename.v0 the semantics of the last arch field ("bits") was unclear # since this format predates float64 builds, we just force it to 32 regsub -- {-[0-9]+$} ${a} {-32} a lappend archs ${a} } if { "x${archs}${version}" == "x" } { # try again as -v if { ! [ regexp "(.*)-v(.*)" ${pkgver} _ pkgname version ] } { set pkgname ${pkgver} set version "" } } } return [list ${pkgname} ${version} ${archs}] } # split filename extension from deken-packagefilename proc ::deken::utilities::get_filenameextension {filename} { if { [ regexp {.*(\.tar\.[^.]*)$} ${filename} _ ext ] } { return ${ext} } return [file extension ${filename}] } # ###################################################################### # ################ preferences ######################################### # ###################################################################### proc ::deken::preferences::create_sources_entry {toplevel} { # 3 panels # - [ ] primary server # - [ ] secondary URLs (list editable) # (all URLs are searched if checked) # - [ ] ephemeral URLs (list multiselectable) # (if checked, and URLs are selected, only these are searched) # (if checked, and no URLs are selected, all are searched) set frame [::deken::preferences::newwidget ${toplevel}.servers] labelframe ${frame} -text [_ "Search URLs:" ] -padx 5 -pady 5 -borderwidth 1 set numframes 0 if {1} { set f [::deken::preferences::newwidget ${frame}.primary] # primary URL labelframe ${f} -borderwidth 0 checkbutton ${f}.use -text "${::deken::search::dekenserver::url_primary}" \ -variable ::deken::preferences::use_url_primary ${f} configure -labelwidget ${f}.use pack ${f} -anchor "w" -fill both -expand 1 -side top pack ${f}.use -anchor "w" -side left incr numframes } if {0} { set f [::deken::preferences::newwidget ${frame}.secondary] # secondary URLs labelframe ${f} -borderwidth 0 checkbutton ${f}.use \ -variable ::deken::preferences::use_urls_secondary button ${f}.open \ -command [list ::deken::preferences::urls2_frame_create ${toplevel}] \ -text [_ "Additional search URLs" ] ${f} configure -labelwidget ${f}.use pack ${f} -anchor "w" -fill both -expand 1 -side left pack ${f}.use -anchor "w" -side left pack ${f}.open -anchor "w" -side left incr numframes } if {0} { set f [::deken::preferences::newwidget ${frame}.ephemeral] # ephemeral URLs labelframe ${f} -borderwidth 0 checkbutton ${f}.use \ -variable ::deken::preferences::use_urls_ephemeral button ${f}.open -text [_ "Ephemeral search URLs" ] ${f} configure -labelwidget ${f}.use pack ${f} -anchor "w" -fill both -expand 1 -side left pack ${f}.use -anchor "w" -side left pack ${f}.open -anchor "w" -side left incr numframes } if { ${numframes} > 1 } { return ${frame} } return "" } proc ::deken::preferences::newwidget {basename} { # calculate a widget name that has not yet been taken set i 0 while {[winfo exists ${basename}${i}]} {incr i} return ${basename}${i} } proc ::deken::preferences::urls2_frame_create {toplevel} { set title [_ "Additional search URLs"] set win ${toplevel}.urls2 destroy $win toplevel $win wm title $win "deken: ${title}" ::scrollbox::make ${win} ${::deken::preferences::urls_secondary} {} {} #::deken::preferences::make_applybutton_frame ${win} \ # [list ::deken::preferences::urls2_frame_apply ${win}] set cmd [list ::scrollboxwindow::apply ${win} ::deken::preferences::set_urls_secondary] ::deken::preferences::make_applybutton_frame ${win} \ "${cmd}; destroy ${win}" bind ${win} [list after idle [list ::deken::preferences::cancel $win]] } proc ::deken::preferences::set_urls_secondary {urls} { set ::deken::preferences::urls_secondary ${urls} } proc ::deken::preferences::create_pathpad {toplevel row {padx 2} {pady 2}} { set pad [::deken::preferences::newwidget ${toplevel}.pad] frame ${pad} -relief groove -borderwidth 2 -width 2 -height 2 grid ${pad} -sticky ew -row ${row} -column 0 -columnspan 3 -padx ${padx} -pady ${pady} } proc ::deken::preferences::create_packpad {toplevel {padx 2} {pady 2} } { set mypad [::deken::preferences::newwidget ${toplevel}.pad] frame ${mypad} pack ${mypad} -padx ${padx} -pady ${pady} -expand 1 -fill "y" return ${mypad} } proc ::deken::preferences::userpath_doit { winid } { set installdir [::deken::do_prompt_installdir ${::deken::preferences::userinstallpath} ${winid}] if { "${installdir}" != "" } { set ::deken::preferences::userinstallpath "${installdir}" } } proc ::deken::preferences::path_doit {rdb ckb path {mkdir true}} { # handler for the check/create button # if the path does not exist, disable the radiobutton and suggest to Create it # if the path exists, check whether it is writable # if it is writable, enable the radiobutton and disable the check/create button # if it is not writable, keep the radiobutton disabled and suggest to (Re)Check ${ckb} configure -state normal ${rdb} configure -state disabled if { ! [file exists ${path}] } { ${ckb} configure -text [_ "Create"] if { ${mkdir} } { catch { file mkdir ${path} } } } if { [file exists ${path}] } { ${ckb} configure -text [_ "Check"] } if { [::deken::utilities::is_writabledir ${path} ] } { ${ckb} configure -state disabled ${rdb} configure -state normal } } proc ::deken::preferences::update_displaypath {var value} { set ${var} ${value} } proc ::deken::preferences::create_pathentry {toplevel row var path {generic false}} { # only add absolute paths to the pathentries set xpath [ ::deken::utilities::expandpath ${path} ] if {! ${generic}} { if { [file pathtype ${xpath}] != "absolute"} { return } } set rdb [::deken::preferences::newwidget ${toplevel}.path] set chk [::deken::preferences::newwidget ${toplevel}.doit] set pad [::deken::preferences::newwidget ${toplevel}.pad] radiobutton ${rdb} -value ${path} -text "${path}" -variable ${var} \ -command [list ::deken::preferences::update_displaypath ${var}_x ${path}] frame ${pad} button ${chk} -text [_ "Browse" ] -command "::deken::preferences::path_doit ${rdb} ${chk} ${xpath}" grid ${rdb} -sticky "w" -row ${row} -column 2 grid ${pad} -sticky "" -row ${row} -column 1 -padx 10 grid ${chk} -sticky nsew -row ${row} -column 0 if {! ${generic}} { ::deken::preferences::path_doit ${rdb} ${chk} ${xpath} false } return [list ${rdb} ${chk}] } proc ::deken::preferences::fill_frame {winid} { ::deken::preferences::create ${winid} } proc ::deken::preferences::create_pathframe {cnv winid} { canvas ${cnv}.cnv \ -confine true scrollbar ${cnv}.scrollv \ -command "${cnv}.cnv yview" scrollbar ${cnv}.scrollh \ -orient horizontal \ -command "${cnv}.cnv xview" ${cnv}.cnv configure \ -xscrollincrement 0 \ -xscrollcommand " ${cnv}.scrollh set" ${cnv}.cnv configure \ -yscrollincrement 0 \ -yscrollcommand " ${cnv}.scrollv set" \ pack ${cnv}.cnv -side left -fill both -expand 1 pack ${cnv}.scrollv -side right -fill "y" pack ${cnv}.scrollh -side bottom -fill "x" -before ${cnv}.cnv set pathsframe [frame ${cnv}.cnv.f] set row 0 ### dekenpath: directory-chooser # FIXME: should we ask user to add chosen directory to PATH? set pathdoit [::deken::preferences::create_pathentry ${pathsframe} ${row} ::deken::preferences::installpath "USER" true] incr row [lindex ${pathdoit} 0] configure \ -foreground blue \ -value "USER" \ -textvariable ::deken::preferences::userinstallpath \ -variable ::deken::preferences::installpath \ -command {::deken::preferences::update_displaypath ::deken::preferences::installpath_x ${::deken::preferences::userinstallpath}} [lindex ${pathdoit} 1] configure \ -text [_ "Browse" ] \ -command "::deken::preferences::userpath_doit ${winid}" ### dekenpath: default directories if {[namespace exists ::pd_docsdir] && [::pd_docsdir::externals_path_is_valid]} { set xpath [::pd_docsdir::get_externals_path] if { [llength ${xpath}] } { ::deken::preferences::create_pathpad ${pathsframe} ${row} incr row } foreach p ${xpath} { ::deken::preferences::create_pathentry ${pathsframe} ${row} ::deken::preferences::installpath ${p} incr row } } if { [llength ${::sys_staticpath}] } { ::deken::preferences::create_pathpad ${pathsframe} ${row} incr row } set extradir [file join ${::sys_libdir} extra ] foreach p ${::sys_staticpath} { if { [file normalize ${p}] == ${extradir} } { set p [file join @PD_PATH@ extra] } ::deken::preferences::create_pathentry ${pathsframe} ${row} ::deken::preferences::installpath ${p} incr row } if {[llength ${::sys_searchpath}]} { ::deken::preferences::create_pathpad ${pathsframe} ${row} incr row } foreach p ${::sys_searchpath} { ::deken::preferences::create_pathentry ${pathsframe} ${row} ::deken::preferences::installpath ${p} incr row } pack ${pathsframe} -fill "x" ${cnv}.cnv create window 0 0 -anchor "nw" -window ${pathsframe} } proc ::deken::preferences::create_pathwindow {parentwin} { set winid ${parentwin}.pathwindow if {[winfo exists ${winid}]} { wm deiconify ${winid} raise ${winid} } else { toplevel ${winid} -class DialogWindow bind ${winid} {after idle {::deken::preferences::cancel %W}} wm title ${winid} [_ "Deken Installation Target"] frame ${winid}.frame pack ${winid}.frame -side top -padx 6 -pady 3 -fill both -expand true ::deken::preferences::create_pathframe ${winid}.frame ${parentwin} frame ${winid}.bframe pack ${winid}.bframe -side bottom -fill "x" -pady 2m button ${winid}.bframe.close -text [_ "Close" ] \ -command [list ::deken::preferences::cancel ${winid}] pack ${winid}.bframe.close -side right -expand 1 -fill "x" -padx 15 -ipadx 10 } } proc ::deken::preferences::create {winid} { # urgh...we want to know when the window gets drawn, # so we can query the size of the pathentries canvas # in order to get the scrolling-region right!!! # this seems to be so wrong... bind ${winid} "::deken::preferences::mapped %W" ::deken::bind_globalshortcuts ${winid} set ::deken::preferences::installpath ${::deken::installpath} set ::deken::preferences::installpath_x ${::deken::installpath} set ::deken::preferences::hideforeignarch ${::deken::hideforeignarch} set ::deken::preferences::hideoldversions ${::deken::hideoldversions} if { ${::deken::userplatform} == "" } { set ::deken::preferences::platform DEFAULT set ::deken::preferences::userplatform [ ::deken::platform2string ] } else { set ::deken::preferences::platform USER set ::deken::preferences::userplatform ${::deken::userplatform} } set ::deken::preferences::installpath USER set ::deken::preferences::userinstallpath ${::deken::installpath} set ::deken::preferences::show_readme ${::deken::show_readme} set ::deken::preferences::keep_package ${::deken::keep_package} set ::deken::preferences::verify_sha256 ${::deken::verify_sha256} set ::deken::preferences::remove_on_install ${::deken::remove_on_install} set ::deken::preferences::add_to_path ${::deken::add_to_path} set ::deken::preferences::add_to_path_temp ${::deken::preferences::add_to_path} set ::deken::preferences::urls_secondary ${::deken::search::dekenserver::urls_secondary} set ::deken::preferences::urls_ephemeral ${::deken::search::dekenserver::urls_ephemeral} set ::deken::preferences::use_url_primary ${::deken::search::dekenserver::use_url_primary} set ::deken::preferences::use_urls_secondary ${::deken::search::dekenserver::use_urls_secondary} set ::deken::preferences::use_urls_ephemeral ${::deken::search::dekenserver::use_urls_ephemeral} # this dialog allows us to select: # - which directory to extract to # - including all (writable) elements from ${::sys_staticpath} # and option to create each of them # - a directory chooser # - whether to delete directories before re-extracting # - whether to filter-out non-matching architectures labelframe ${winid}.installdir -text [_ "Install externals to directory:" ] \ -borderwidth 1 -padx 5 -pady 5 if { 1 } { set readonly_color [lindex [${winid} configure -background] end] pack ${winid}.installdir -fill x -anchor s # -padx {2m 4m} -pady 2m frame ${winid}.installdir.path pack ${winid}.installdir.path -fill x entry ${winid}.installdir.path.entry -textvariable ::deken::preferences::installpath_x \ -takefocus 0 -state readonly -readonlybackground $readonly_color button ${winid}.installdir.path.browse -text [_ "Browse"] \ -command [list ::deken::preferences::create_pathwindow ${winid}] pack ${winid}.installdir.path.browse -side right -fill x -ipadx 8 pack ${winid}.installdir.path.entry -side right -expand 1 -fill x # scroll to right for long paths ${winid}.installdir.path.entry xview moveto 1 } else { ::deken::preferences::create_pathframe ${winid}.installdir ${winid} } pack ${winid}.installdir -fill both ## installation options labelframe ${winid}.install -text [_ "Installation options:" ] -padx 5 -pady 5 -borderwidth 1 pack ${winid}.install -side top -fill "x" -anchor "w" checkbutton ${winid}.install.verify256 -text [_ "Try to verify the libraries' checksum before (re)installing them"] \ -variable ::deken::preferences::verify_sha256 pack ${winid}.install.verify256 -anchor "w" checkbutton ${winid}.install.remove -text [_ "Try to remove libraries before (re)installing them"] \ -variable ::deken::preferences::remove_on_install pack ${winid}.install.remove -anchor "w" checkbutton ${winid}.install.readme -text [_ "Show README of newly installed libraries (if present)"] \ -variable ::deken::preferences::show_readme pack ${winid}.install.readme -anchor "w" checkbutton ${winid}.install.keeppackage -text [_ "Keep package files after installation"] \ -variable ::deken::preferences::keep_package pack ${winid}.install.keeppackage -anchor "w" checkbutton ${winid}.install.add_to_path -text [_ "Add newly installed libraries to Pd's search path"] \ -variable ::deken::preferences::add_to_path catch { ${winid}.install.add_to_path configure \ -tristatevalue 1 \ -onvalue 2 \ -command {set ::deken::preferences::add_to_path \ [set ::deken::preferences::add_to_path_temp \ [::deken::utilities::tristate ${::deken::preferences::add_to_path_temp} 1 0]]} set msg "- Always add to search path\n- Never add to search path\n- Prompt before adding" bind ${winid}.install.add_to_path "::deken::balloon::show ${winid}.install_balloon %X \[winfo rooty %W\] \{${msg}\} 0 30" bind ${winid}.install.add_to_path [list ::deken::balloon::hide ${winid}.install_balloon] } stdout pack ${winid}.install.add_to_path -anchor "w" ## platform filter settings labelframe ${winid}.platform -text [_ "Platform settings:" ] -padx 5 -pady 5 -borderwidth 1 pack ${winid}.platform -side top -fill "x" -anchor "w" # default architecture vs user-defined arch radiobutton ${winid}.platform.default -value "DEFAULT" \ -text [format [_ "Default platform: %s" ] [::deken::platform2string ] ] \ -variable ::deken::preferences::platform \ -command "${winid}.platform.userarch.entry configure -state disabled" pack ${winid}.platform.default -anchor "w" frame ${winid}.platform.userarch radiobutton ${winid}.platform.userarch.radio -value "USER" \ -text [_ "User-defined platform:" ] \ -variable ::deken::preferences::platform \ -command "${winid}.platform.userarch.entry configure -state normal" entry ${winid}.platform.userarch.entry -textvariable ::deken::preferences::userplatform if { "${::deken::preferences::platform}" == "DEFAULT" } { ${winid}.platform.userarch.entry configure -state disabled } pack ${winid}.platform.userarch -anchor "w" pack ${winid}.platform.userarch.radio -side left pack ${winid}.platform.userarch.entry -side right -fill "x" # hide non-matching architecture? ::deken::preferences::create_packpad ${winid}.platform 2 10 checkbutton ${winid}.platform.hide_foreign -text [_ "Hide foreign architectures"] \ -variable ::deken::preferences::hideforeignarch pack ${winid}.platform.hide_foreign -anchor "w" checkbutton ${winid}.platform.only_newest -text [_ "Only show the newest version of a library\n(treats other versions like foreign architectures)"] \ -variable ::deken::preferences::hideoldversions -justify "left" pack ${winid}.platform.only_newest -anchor "w" # search URLs set sourceframe [::deken::preferences::create_sources_entry ${winid}] if { ${sourceframe} ne {} } { pack ${sourceframe} -anchor "w" -fill both -expand 1 } } proc ::deken::preferences::mapped {winid} { set cnv ${winid}.installdir.cnv catch { set bbox [${cnv} bbox all] if { "${bbox}" != "" } { ${cnv} configure -scrollregion ${bbox} } } stdout } proc ::deken::preferences::make_applybutton_frame {winid okproc {cancelproc {}} {applyproc {}}} { # the Apply/OK/Cancel buttons if {${::windowingsystem} eq "aqua"} { # no "Apply" on macOS... set applyproc {} } if { ${cancelproc} eq {} } { set cancelproc [list destroy ${winid}] } # Use two frames for the buttons, since we want them both bottom and right frame ${winid}.nb pack ${winid}.nb -side bottom -fill "x" -pady 2m # buttons frame ${winid}.nb.buttonframe pack ${winid}.nb.buttonframe -side right -fill "x" -padx 2m button ${winid}.nb.buttonframe.cancel -text [_ "Cancel"] \ -command ${cancelproc} pack ${winid}.nb.buttonframe.cancel -side left -expand 1 -fill "x" -padx 15 -ipadx 10 if { ${applyproc} ne {} } { button ${winid}.nb.buttonframe.apply -text [_ "Apply"] \ -command ${applyproc} pack ${winid}.nb.buttonframe.apply -side left -expand 1 -fill "x" -padx 15 -ipadx 10 } if { ${okproc} ne {} } { button ${winid}.nb.buttonframe.ok -text [_ "OK"] \ -command ${okproc} pack ${winid}.nb.buttonframe.ok -side left -expand 1 -fill "x" -padx 15 -ipadx 10 } } proc ::deken::preferences::show {{winid .deken_preferences}} { if {[winfo exists ${winid}]} { wm deiconify ${winid} raise ${winid} } else { toplevel ${winid} -class DialogWindow wm title ${winid} [format [_ "Deken %s Preferences"] ${::deken::version}] frame ${winid}.frame pack ${winid}.frame -side top -padx 6 -pady 3 -fill both -expand true bind ${winid} {after idle {::deken::preferences::cancel %W}} ::deken::preferences::create ${winid}.frame # the Apply/OK/Cancel buttons ::deken::preferences::make_applybutton_frame ${winid} \ [list ::deken::preferences::ok ${winid}] \ [list ::deken::preferences::cancel ${winid}] \ [list ::deken::preferences::apply ${winid}] } } proc ::deken::preferences::apply {winid} { set installpath "${::deken::preferences::installpath}" if { "${installpath}" == "USER" } { set installpath "${::deken::preferences::userinstallpath}" } ::deken::set_installpath "${installpath}" set plat "" if { "${::deken::preferences::platform}" == "USER" } { set plat "${::deken::preferences::userplatform}" } ::deken::set_platform_options ${plat} ${::deken::preferences::hideforeignarch} ${::deken::preferences::hideoldversions} ::deken::set_install_options \ "${::deken::preferences::remove_on_install}" \ "${::deken::preferences::show_readme}" \ "${::deken::preferences::add_to_path}" \ "${::deken::preferences::keep_package}" \ "${::deken::preferences::verify_sha256}" ::deken::set_search_urls \ ${::deken::preferences::use_url_primary} \ ${::deken::preferences::use_urls_secondary} \ ${::deken::preferences::urls_secondary} \ ${::deken::preferences::use_urls_ephemeral} \ ${::deken::preferences::urls_ephemeral} } proc ::deken::preferences::cancel {winid} { ## FIXXME properly close the window/frame (for reuse in a tabbed pane) destroy ${winid} } proc ::deken::preferences::ok {winid} { ::deken::preferences::apply ${winid} ::deken::preferences::cancel ${winid} } # ###################################################################### # ################ core ################################################ # ###################################################################### if { [ catch { set ::deken::installpath [::pd_guiprefs::read dekenpath] } stdout ] } { # this is a Pd without the new GUI-prefs proc ::deken::set_installpath {installdir} { set ::deken::installpath ${installdir} } proc ::deken::set_platform_options {platform hideforeignarch {hideoldversions 0}} { set ::deken::userplatform ${platform} set ::deken::hideforeignarch [::deken::utilities::bool ${hideforeignarch} ] set ::deken::hideoldversions [::deken::utilities::bool ${hideoldversions} ] } proc ::deken::set_install_options {remove readme add keep verify256} { set ::deken::remove_on_install [::deken::utilities::bool ${remove}] set ::deken::show_readme [::deken::utilities::bool ${readme}] set ::deken::add_to_path [::deken::utilities::tristate ${add} 0 0] set ::deken::keep_package [::deken::utilities::bool ${keep}] set ::deken::verify_sha256 [::deken::utilities::bool ${verify256}] } proc ::deken::set_search_urls {use_primary use_secondaries secondaries use_ephemerals ephemerals} { set ::deken::search::dekenserver::use_url_primary [::deken::utilities::bool ${use_primary}] set ::deken::search::dekenserver::use_urls_secondary [::deken::utilities::bool ${use_secondaries}] set ::deken::search::dekenserver::urls_secondary ${secondaries} set ::deken::search::dekenserver::use_urls_ephemeral [::deken::utilities::bool ${use_ephemerals}] set ::deken::search::dekenserver::urls_ephemeral ${ephemerals} } } else { catch {set ::deken::installpath [lindex ${::deken::installpath} 0]} # Pd has a generic preferences system, that we can use proc ::deken::set_installpath {installdir} { set ::deken::installpath ${installdir} ::pd_guiprefs::write dekenpath [list ${installdir}] } # user requested platform (empty = DEFAULT) set ::deken::userplatform [::pd_guiprefs::read deken_platform] catch {set ::deken::userplatform [lindex ${deken::userplatform} 0 ]} # urgh, on macOS an empty :deken::userplatform ({}, which is promoted to [list {}] on save) # got saved as a literal "{}" (actually "\\\\{\\\\}") # which then gets restored as "\\{\\}"... # the bogus write behaviour was fixed with v0.9.8, but we need to handle old prefs... set ::deken::userplatform [string trim [string trim ${::deken::userplatform} "\\\{\}" ] ] set ::deken::hideforeignarch [::deken::utilities::bool [::pd_guiprefs::read deken_hide_foreign_archs] 1] set ::deken::hideoldversions [::deken::utilities::bool [::pd_guiprefs::read deken_hide_old_versions] 1] proc ::deken::set_platform_options {platform hideforeignarch {hideoldversions 0}} { set ::deken::userplatform ${platform} if { ${platform} == "" } { set platformlist [list] } else { set platformlist [list ${platform}] } set ::deken::hideforeignarch [::deken::utilities::bool ${hideforeignarch} ] set ::deken::hideoldversions [::deken::utilities::bool ${hideoldversions} ] ::pd_guiprefs::write deken_platform ${platformlist} ::pd_guiprefs::write deken_hide_foreign_archs ${::deken::hideforeignarch} ::pd_guiprefs::write deken_hide_old_versions ${::deken::hideoldversions} } set ::deken::remove_on_install [::deken::utilities::bool [::pd_guiprefs::read deken_remove_on_install] 1] set ::deken::show_readme [::deken::utilities::bool [::pd_guiprefs::read deken_show_readme] 1] set ::deken::keep_package [::deken::utilities::bool [::pd_guiprefs::read deken_keep_package] 0] set ::deken::verify_sha256 [::deken::utilities::bool [::pd_guiprefs::read deken_verify_sha256] 1] set ::deken::add_to_path [::deken::utilities::tristate [::pd_guiprefs::read deken_add_to_path] ] proc ::deken::set_install_options {remove readme path keep verify256} { set ::deken::remove_on_install [::deken::utilities::bool ${remove}] set ::deken::show_readme [::deken::utilities::bool ${readme}] set ::deken::add_to_path [::deken::utilities::tristate ${path}] set ::deken::keep_package [::deken::utilities::bool ${keep}] set ::deken::verify_sha256 [::deken::utilities::bool ${verify256}] ::pd_guiprefs::write deken_remove_on_install "${::deken::remove_on_install}" ::pd_guiprefs::write deken_show_readme "${::deken::show_readme}" ::pd_guiprefs::write deken_add_to_path "${::deken::add_to_path}" ::pd_guiprefs::write deken_keep_package "${::deken::keep_package}" ::pd_guiprefs::write deken_verify_sha256 "${::deken::verify_sha256}" } set ::deken::search::dekenserver::use_url_primary [::deken::utilities::bool [pd_guiprefs::read dekensearch_useprimaryurl] 1] set ::deken::search::dekenserver::use_urls_secondary [::deken::utilities::bool [pd_guiprefs::read dekensearch_usesecondaryurls] 0] set ::deken::search::dekenserver::urls_secondary [pd_guiprefs::read dekensearch_secondaryurls] set ::deken::search::dekenserver::use_urls_ephemeral [::deken::utilities::bool [pd_guiprefs::read dekensearch_useephemeralurls] 0] set ::deken::search::dekenserver::urls_ephemeral [pd_guiprefs::read dekensearch_ephemeralurls] proc ::deken::set_search_urls {use_primary use_secondaries secondaries use_ephemerals ephemerals} { set ::deken::search::dekenserver::use_url_primary [::deken::utilities::bool ${use_primary}] set ::deken::search::dekenserver::use_urls_secondary [::deken::utilities::bool ${use_secondaries}] set ::deken::search::dekenserver::urls_secondary ${secondaries} set ::deken::search::dekenserver::use_urls_ephemeral [::deken::utilities::bool ${use_ephemerals}] set ::deken::search::dekenserver::urls_ephemeral ${ephemerals} ::pd_guiprefs::write dekensearch_useprimaryurl "${::deken::search::dekenserver::use_url_primary}" ::pd_guiprefs::write dekensearch_usesecondaryurls "${::deken::search::dekenserver::use_urls_secondary}" ::pd_guiprefs::write dekensearch_secondaryurls "${::deken::search::dekenserver::urls_secondary}" ::pd_guiprefs::write dekensearch_useephemeralurls "${::deken::search::dekenserver::use_urls_ephemeral}" ::pd_guiprefs::write dekensearch_ephemeralurls "${::deken::search::dekenserver::urls_ephemeral}" } } proc ::deken::normalize_result {title cmd {match 1} {subtitle ""} {statusline ""} {contextcmd {}} {pkgname ""} {version ""} {uploader ""} {timestamp ""} args} { ## normalize a search-result # the function parameters are guaranteed to be a stable API (with the exception of ) # but the value returned by this function is an implementation detail # the primary line displayed for the search-result # - <cmd> the full command to run to install the library # - <match> boolean value to indicate whether this entry matches the current architecture # - <subtitle> additional text to be shown under the <name> # - <statusline> additional text to be shown in the STATUS line if the mouse hovers over the result # - <contextcmd> the full command to be executed when the user right-clicks the menu-entry # - <pkgname> the library name (typically this gets parsed from the package filename) # - <uploader> who provided the package # - <timestamp> the upload date of the package # - <args> RESERVED FOR FUTURE USE (this is a variadic placeholder. do not use!) return [list "" ${title} ${cmd} ${match} ${subtitle} ${statusline} ${contextcmd} ${pkgname} ${version} ${uploader} ${timestamp}] } # find an install path, either from prefs or on the system # returns an empty string if nothing was found proc ::deken::find_installpath {{ignoreprefs false}} { set installpath "" if { [ info exists ::deken::installpath ] && !${ignoreprefs} } { ## any previous choice? return ${::deken::installpath} } if { "${installpath}" == "" } { ## search the default paths set installpath [ ::deken::utilities::get_writabledir ${::sys_staticpath} ] } if { "${installpath}" == "" } { # let's use the first of ${::sys_staticpath}, if it does not exist yet set userdir [lindex ${::sys_staticpath} 0] if { ! [file exists ${userdir} ] } { set installpath ${userdir} } } return ${installpath} } proc ::deken::platform2string {{verbose 0}} { if { ${verbose} } { return $::deken::platform(os)-$::deken::platform(machine)-float$::deken::platform(floatsize) } else { return $::deken::platform(os)-$::deken::platform(machine)-$::deken::platform(floatsize) } } # allow overriding deken platform from Pd-core proc ::deken::set_platform {os machine bits floatsize} { set machine [string tolower ${machine}] set bits [::deken::utilities::int ${bits} $::deken::platform(bits)] set floatsize [::deken::utilities::int ${floatsize} $::deken::platform(floatsize)] if { ${os} != $::deken::platform(os) || ${machine} != $::deken::platform(machine) || ${bits} != $::deken::platform(bits) || ${floatsize} != $::deken::platform(floatsize) } { set ::deken::platform(os) ${os} set ::deken::platform(machine) ${machine} set ::deken::platform(bits) ${bits} set ::deken::platform(floatsize) ${floatsize} set msg [format [_ "Platform re-detected: %s" ] [::deken::platform2string 1] ] ::pdwindow::verbose 0 "\[deken\] ${msg}\n" } if { [info procs ::pdwindow::update_title] ne ""} { after idle {::pdwindow::update_title .pdwindow} } } proc ::deken::versioncompare {a b} { # compares two versions, the Debian way # each version string is split into numeric and non-numeric elements # the elements are compared pairwise # "~" sorts before everything else # sidenote: in practice the version we get here are of the form "<date>/<library>/<version>/<date>" # we probably should only use this version-comparision for the <version> part, # and use 'string compare' for the other parts foreach x [regexp -all -inline {\d+|\D+} [string map {~ \t} ${a}]] y [regexp -all -inline {\d+|\D+} [string map {~ \t} ${b}]] { if { "${x}" == "" } { set x " " } if { "${y}" == "" } { set y " " } if { [catch { set c [dict get {1 0 {0 1} -1 {1 0} 1} [lsort -indices -dictionary -unique [list ${x} ${y}]]] } stdout ] } { # Tcl<8.5 (as found the PowerPC builds) lacks 'dict' and 'lsort -indices' if { [catch { # "string compare" does not sort numerically set c [expr {2 * (${x} > ${y}) + (${x} == ${y}) - 1}] } stdout] } { set c [string compare ${x} ${y}] } } if { ${c} != "0" } {return ${c}} } return 0 } proc ::deken::verify_sha256_gui {url pkgfile} { ## verify that the SHA256 of the ${pkgfile} matches that from ${url} ## in case of failure, this displays a dialog asking the user how to proceed ## (if the preferences indicate we require checking) ## returns ## - 1 on success ## - 0 on failure ## - negative numbers indicate failures to be ignored ## - one digit: user requested ignore ## - -1 user requested ignore via prefs ## - -2 user requested ignore via dialog ## - two digits: unable to verify ## - -10 reference could not be read ## - -20 an exception occurred while verifying ## - three digits: ## - -100 no sha256 verifier implemented set err_msg [format [_ "SHA256 verification of '%s' failed!" ] ${pkgfile} ] set err_title [_ "SHA256 verification failed" ] set err_status [format [_ "Checksum mismatch for '%s'" ] ${url}] while 1 { set hash_ok [::deken::utilities::verify_sha256 ${url} ${pkgfile}] if { ${hash_ok} } { return ${hash_ok} } ::deken::statuspost ${err_status} warn 0 if { ! ${::deken::verify_sha256} } { return -1 } set result [tk_messageBox \ -title ${err_title} \ -message ${err_msg} \ -icon error -type abortretryignore \ -parent ${::deken::winid}] switch -- ${result} { abort { return 0 } ignore { return -2 } } } return 0 } proc ::deken::install_package_from_file {{pkgfile ""}} { set types {} lappend types [list [_ "Deken Packages" ] .dek] lappend types [list [_ "ZIP Files" ] .zip] if {$::tcl_platform(platform) ne "windows"} { lappend types [list [_ "TAR Files" ] {.tgz} ] if {${::windowingsystem} eq "aqua"} { # stupid bug on macOS>=12: an extension with two dots crashes the fileselector lappend types [list [_ "TAR Files" ] {.gz} ] } else { lappend types [list [_ "TAR Files" ] {.tar.gz} ] } } lappend types [list [_ "All Files" ] * ] if { "${pkgfile}" eq ""} { set pkgfile [tk_getOpenFile -defaultextension dek -filetypes ${types}] } if { "${pkgfile}" eq "" } { return } # user picked one # perform checks and install it set pkgfile [file normalize ${pkgfile}] set result [::deken::verify_sha256_gui ${pkgfile} ${pkgfile}] if { ! ${result} } { return } ::deken::install_package ${pkgfile} "" "" 1 } proc ::deken::install_package {fullpkgfile {filename ""} {installdir ""} {keep 1}} { # fullpkgfile: the file to extract # filename : the package file name (usually the basename of ${fullpkgfile}) # but might be different depending on the download) # installdir : where to put stuff into # keep : whether to remove the fullpkgfile after successful extraction if { "${filename}" == "" } { set filename [file tail ${fullpkgfile}] } set installdir [::deken::ensure_installdir ${installdir} ${filename}] set parsedname [::deken::utilities::parse_filename ${filename}] foreach {extname version archs} ${parsedname} {break} set showextname ${extname} set deldir "" set extpath [file join ${installdir} ${extname}] if { ${extname} eq {} } { # cannot remove previous installation, as we couldn't guess the library name set showextname ${filename} } else { set match [::deken::architecture_match "${archs}" ] if { ! ${match} } { set msg [_ "Installing incompatible architecture of '%s'." ${showextname} ] ::deken::post "${msg}" warn if { "${::deken::remove_on_install}" && [file exists ${extpath}] } { set result [tk_messageBox \ -title [_ "Replacing library with incompatible architecture!" ] \ -message [_ "Do you want to replace the library '%1\$s' in '%2\$s' with a version that is incompatible with your computer?" ${showextname} ${installdir}] \ -icon error -type yesnocancel \ -parent ${::deken::winid}] switch -- "${result}" { cancel {return} yes { } no { set installdir [::deken::do_prompt_installdir ${installdir}] if { "${installdir}" == "" } { return } set extpath [file join ${installdir} ${extname}] } } } } if { "${::deken::remove_on_install}" } { ::deken::statuspost [format [_ "Uninstalling previous installation of '%s'" ] ${showextname} ] info if { ! [::deken::utilities::uninstall ${installdir} ${extname}] } { # ouch uninstalling failed. # on msw, lets assume this is because some of the files in the folder are locked. # so move the folder out of the way and proceed set deldir [::deken::utilities::get_tmpfilename ${installdir}] if { [ catch { file mkdir ${deldir} file rename [file join ${installdir} ${extname}] [file join ${deldir} ${extname}] } ] } { ::deken::utilities::debug [format [_ "Temporarily moving %1\$s into %2\$s failed." ] ${showextname} ${deldir} ] set deldir "" } } } } ::deken::statuspost [format [_ "Installing package '%s'" ] ${showextname} ] {} 0 ::deken::syncgui ::deken::progress 0 if { [::deken::utilities::extract ${installdir} ${filename} ${fullpkgfile} ${keep}] > 0 } { ::deken::progressstatus [_ "Installation completed!" ] set msg [format [_ "Successfully installed '%s'!" ] ${showextname} ] ::deken::statuspost "${msg}" {} 0 ::deken::post "" ::pdwindow::post "\[deken\] ${msg}\n" set install_failed 0 } else { ::deken::progressstatus [_ "Installation failed!" ] set msg [format [_ "Failed to install '%s'!" ] ${showextname} ] ::deken::statuspost ${msg} error 0 tk_messageBox \ -title [_ "Package installation failed" ] \ -message "${msg}" \ -icon error -type ok \ -parent ${::deken::winid} set install_failed 1 } if { "${deldir}" != "" } { # try getting rid of the directory to be deleted # we already tried once (and failed), so this time we iterate over each file set rmerrors [::deken::utilities::rmrecursive ${deldir}] # and if there are still files around, ask the user to delete them. if { ${rmerrors} > 0 } { set result [tk_messageBox \ -message [format [_ "Failed to completely remove %1\$s.\nPlease manually remove the directory %2\$s after quitting Pd." ] ${showextname} ${deldir}] \ -icon warning -type okcancel -default ok \ -parent ${::deken::winid}] switch -- ${result} { ok { ::pd_menucommands::menu_openfile ${deldir} } } } set deldir "" } if { ${install_failed} } { return } if { ${extname} eq {} } { # failed to properly parse packagefile, so we do not know the libraryname # therefore we cannot actually show the readme nor add the path return } if { "${::deken::show_readme}" } { foreach ext {pd html pdf txt} { set r [file join ${extpath} "README.deken.${ext}"] if {[file exists ${r}]} { if { "${ext}" == "pd" } { set directory [file normalize [file dirname ${r}]] set basename [file tail ${r}] pdsend "pd open [enquote_path ${basename}] [enquote_path ${directory}]" } else { pd_menucommands::menu_openfile ${r} } break } } } if { "${::deken::add_to_path}" } { # add to the search paths? bail if the version of pd doesn't support it if {[uplevel 1 info procs add_to_searchpaths] eq ""} {return} if {![file exists ${extpath}]} { ::deken::utilities::debug [format [_ "Unable to add %s to search paths"] ${extname}] return } set result yes if { ${::deken::add_to_path} > 1 } { set result yes } else { set result [tk_messageBox \ -message [format [_ "Add %s to the Pd search paths?" ] ${extname}] \ -icon question -type yesno -default yes \ -parent ${::deken::winid}] } switch -- "${result}" { yes { add_to_searchpaths [file join ${installdir} ${extname}] ::deken::utilities::debug [format [_ "Added %s to search paths"] ${extname}] # if this version of pd supports it, try refreshing the helpbrowser if {[uplevel 1 info procs ::helpbrowser::refresh] ne ""} { ::helpbrowser::refresh } } no { return } } } } ##### GUI ######## proc ::deken::bind_globalshortcuts {toplevel} { # this should probably only be called if toplevel is indeed a toplevel if { ${toplevel} eq [winfo toplevel ${toplevel}] } { bind ${toplevel} <${::modifier}-Key-w> [list destroy ${toplevel}] bind ${toplevel} <Escape> [list after idle [list destroy ${toplevel}]] } } proc ::deken::status {{msg ""} {timeout 5000}} { after cancel ${::deken::statustimer} if {"" ne ${msg}} { set ::deken::statustext "${msg}" if { ${timeout} != "0" } { set ::deken::statustimer [after ${timeout} [list set "::deken::statustext" ""]] } } else { set ::deken::statustext "" } } proc ::deken::progressstatus {{msg ""} {timeout 5000}} { after cancel ${::deken::progresstimer} if {"" ne ${msg}} { set ::deken::progresstext "${msg}" if { ${timeout} != "0" } { set ::deken::progresstimer [after ${timeout} [list set "::deken::progresstext" ""]] } } else { set ::deken::progresstext "" } } proc ::deken::syncgui {} { update idletasks } proc ::deken::scrollup {} { variable infoid if { [winfo exists ${infoid}] } { ${infoid} see 0.0 } } proc ::deken::post {msg args} { variable infoid if { [winfo exists ${infoid}] } { ${infoid} insert end "${msg}\n" ${args} ${infoid} see end } } proc ::deken::statuspost {msg {tag info} {timeout 5000}} { post ${msg} ${tag} status ${msg} ${timeout} } proc ::deken::clearpost {} { variable infoid if { [winfo exists ${infoid}] } { ${infoid} delete 1.0 end } set ::deken::selected {} } proc ::deken::post_result {msg {tag ""}} { variable resultsid if { [winfo exists ${resultsid}] } { ${resultsid} insert end "${msg}\n" ${tag} ${resultsid} see end } } proc ::deken::bind_resulttag {tagname key cmd} { variable resultsid if { [winfo exists ${resultsid}] } { ${resultsid} tag bind ${tagname} ${key} ${cmd} } } proc ::deken::highlightable_resulttag {tagname} { variable resultsid if { [winfo exists ${resultsid}] } { ::deken::bind_resulttag ${tagname} <Enter> \ "${resultsid} tag add highlight [ ${resultsid} tag ranges ${tagname} ]" ::deken::bind_resulttag ${tagname} <Leave> \ "${resultsid} tag remove highlight [ ${resultsid} tag ranges ${tagname} ]" # make sure that the 'highlight' tag is topmost ${resultsid} tag raise sel ${resultsid} tag raise highlight } } proc ::deken::bind_contextmenu {resultsid tagname cmd} { if { [winfo exists ${resultsid}] } { if {${::windowingsystem} eq "aqua"} { ${resultsid} tag bind ${tagname} <2> ${cmd} } else { ${resultsid} tag bind ${tagname} <3> ${cmd} } } } proc ::deken::menu_installselected {resultsid} { set counter 0 foreach {k v} ${::deken::selected} { if { ${v} ne {} } { eval ${v} incr counter } } if { ${counter} == 0 } { ::deken::statuspost [_ "No packages selected for installation."] } elseif { ${counter} > 1 } { ::deken::statuspost [format [_ "Processed %d packages selected for installation."] ${counter} ] } # clear the selection set ::deken::selected {} ::deken::clear_selection ${resultsid} ::deken::update_installbutton ${::deken::winid} } proc ::deken::menu_uninstall_package {winid pkgname installpath} { ::deken::show_tab ${winid} info ::deken::statuspost [format [_ "Uninstalling previous installation of '%s'" ] ${pkgname} ] info ::deken::utilities::uninstall ${installpath} ${pkgname} } proc ::deken::do_prompt_installdir {path {winid {}}} { set msg [_ "Install externals to directory:"] if { ${winid} eq {} } { set winid ${::deken::winid} } if {[winfo exists ${winid}]} { tk_chooseDirectory -title "${msg}" -initialdir ${path} -parent ${winid} } else { tk_chooseDirectory -title "${msg}" -initialdir ${path} } } proc ::deken::prompt_installdir {} { set installdir [::deken::do_prompt_installdir ${::fileopendir}] if { "${installdir}" != "" } { ::deken::set_installpath ${installdir} return 1 } return 0 } proc ::deken::update_searchbutton {winid} { if { [${winid}.searchbit.entry get] == "" } { ${winid}.searchbit.button configure -text [_ "Show all" ] } else { ${winid}.searchbit.button configure -text [_ "Search" ] } } proc ::deken::update_installbutton {winid} { set installbutton ${winid}.status.install if { ! [winfo exists ${installbutton}] } { return } set counter 0 foreach {a b} ${::deken::selected} { if {${b} ne {} } { incr counter } } if { ${counter} > 0 } { ${installbutton} configure -state normal -text [format [_ "Install (%d)" ] ${counter}] } else { ${installbutton} configure -state disabled -text [_ "Install" ] } } proc ::deken::progress {x} { ::deken::statuspost [format [_ "%s%% of download completed"] ${x}] } # this function gets called when the menu is clicked proc ::deken::open_searchui {winid} { if {[winfo exists ${winid}]} { wm deiconify ${winid} raise ${winid} } else { variable resultsid variable infoid ::deken::create_dialog ${winid} ::deken::bind_globalshortcuts ${winid} foreach dndid [list ${winid}.tab ${winid}.results] { if { [winfo exists ${dndid}] } { ::deken::utilities::dnd_init ${dndid} } } ${infoid} tag configure error -foreground red ${infoid} tag configure warn -foreground orange ${infoid} tag configure info -foreground black ${infoid} tag configure debug -foreground grey ${infoid} tag configure dekenurl -foreground blue ${infoid} tag bind dekenurl <1> "pd_menucommands::menu_openfile https://deken.puredata.info/" ${infoid} tag bind dekenurl <Enter> "${infoid} tag configure dekenurl -underline 1" ${infoid} tag bind dekenurl <Leave> "${infoid} tag configure dekenurl -underline 0" ${resultsid} tag configure highlight -foreground blue ${resultsid} tag configure archmatch ${resultsid} tag configure noarchmatch -foreground grey } ::deken::clearpost ::deken::post [_ "Enter an exact library or object name."] info set msg [_ "e.g. 'freeverb~'"] ::deken::post "\t${msg}" info ::deken::post [_ "Use the '*' wildcard to match any number of characters."] info set msg [_ "e.g. '*-plugin' will match 'deken-plugin' (and more)."] ::deken::post "\t${msg}" info ::deken::post [_ "You can restrict the search to only-libraries or only-objects."] info ::deken::post [_ "To get a list of all available externals, try an empty search."] info ::deken::post "" info ::deken::post [_ "Right-clicking a search result will give you more options..." ] info ::deken::post "" info ::deken::post [_ "You can also search for libraries & objects via your web browser:" ] info ::deken::post "https://deken.puredata.info" dekenurl } # build the externals search dialog window proc ::deken::create_dialog {winid} { variable resultsid toplevel ${winid} -class DialogWindow set ::deken::winid ${winid} set title [_ "Find externals"] wm title ${winid} "deken - ${title}" wm geometry ${winid} 670x550 wm minsize ${winid} 230 360 wm transient ${winid} ${winid} configure -padx 10 -pady 5 set m ${winid}_menu destroy ${m} menu ${m} menu ${m}.file ${m} add cascade -label [_ [string totitle "file"]] -underline 0 -menu ${m}.file ${m}.file add command -label [_ "Install DEK file..." ] -command "::deken::install_package_from_file" menu ${m}.edit ${m} add cascade -label [_ [string totitle "edit"]] -underline 0 -menu ${m}.edit ${m}.edit add command -label [_ "Preferences..." ] -command "::deken::preferences::show" ${winid} configure -menu ${m} frame ${winid}.searchbit pack ${winid}.searchbit -side top -fill "x" entry ${winid}.searchbit.entry -font 18 -relief sunken -highlightthickness 1 -highlightcolor blue pack ${winid}.searchbit.entry -side left -padx 6 -fill "x" -expand true bind ${winid}.searchbit.entry <Key-Return> "::deken::initiate_search ${winid}" bind ${winid}.searchbit.entry <KeyRelease> "::deken::update_searchbutton ${winid}" focus ${winid}.searchbit.entry button ${winid}.searchbit.button -text [_ "Show all"] -default active -command "::deken::initiate_search ${winid}" pack ${winid}.searchbit.button -side right -padx 6 -pady 3 -ipadx 10 frame ${winid}.objlib pack ${winid}.objlib -side top -fill "x" label ${winid}.objlib.label -text [_ "Search for: "] radiobutton ${winid}.objlib.libraries -text [_ "libraries"] -variable ::deken::searchtype -value libraries radiobutton ${winid}.objlib.objects -text [_ "objects"] -variable ::deken::searchtype -value objects radiobutton ${winid}.objlib.both -text [_ "both"] -variable ::deken::searchtype -value name foreach x {label libraries objects both} { pack ${winid}.objlib.${x} -side left -padx 6 } # for Pd that supports it, add a 'translation' radio if {[uplevel 2 info procs add_to_helppaths] ne ""} { radiobutton ${winid}.objlib.translations -text [_ "translations"] -variable ::deken::searchtype -value translations pack ${winid}.objlib.translations -side left -padx 6 } frame ${winid}.warning pack ${winid}.warning -side top -fill "x" label ${winid}.warning.label -text [_ "Only install externals uploaded by people you trust."] pack ${winid}.warning.label -side left -padx 6 if { [catch { if {${::windowingsystem} eq "aqua" && [::deken::versioncompare 8.6 [info patchlevel]] < 0 && [::deken::versioncompare 8.6.12 [info patchlevel]] > 0 } { ::deken::utilities::debug [_ "Disabling tabbed view: incompatible Tcl/Tk detected"] error [_ "Disabling tabbed view: incompatible Tcl/Tk detected"] } ttk::notebook ${winid}.tab pack ${winid}.tab -side top -padx 6 -pady 3 -fill both -expand true text ${winid}.tab.info -takefocus 0 -cursor hand2 -height 100 -yscrollcommand "${winid}.tab.info.ys set" scrollbar ${winid}.tab.info.ys -orient vertical -command "${winid}.tab.info yview" pack ${winid}.tab.info.ys -side right -fill "y" if { [catch { set treeid ${winid}.tab.results ttk::treeview ${treeid} \ -height 10 \ -selectmode browse \ -columns {version title uploader date} \ -displaycolumns {version uploader date} \ -yscrollcommand "${winid}.tab.results.ys set" ${treeid} heading #0 -text [_ "Library" ] -anchor center -command "::deken::treeresults::columnsort ${treeid}" ${treeid} heading version -text [_ "Version" ] -anchor center -command "::deken::treeresults::columnsort ${treeid} version" ${treeid} heading title -text [_ "Description" ] -anchor center -command "::deken::treeresults::columnsort ${treeid} title" ${treeid} heading uploader -text [_ "Uploader" ] -anchor center -command "::deken::treeresults::columnsort ${treeid} uploader" ${treeid} heading date -text [_ "Date" ] -anchor center -command "::deken::treeresults::columnsort ${treeid} date" ${treeid} column #0 -stretch 0 ${treeid} tag configure library -background lightgrey ${treeid} tag configure noarchmatch -foreground lightgrey ${treeid} tag configure selpkg -background lightblue bind ${treeid} <<TreeviewSelect>> "::deken::treeresults::selection_changed %W" bind ${treeid} <<TreeviewOpen>> "::deken::treeresults::selection_skip %W 1" bind ${treeid} <<TreeviewClose>> "::deken::treeresults::selection_skip %W 1" bind ${treeid} <Motion> "::deken::treeresults::motionevent %W %x %y" bind ${treeid} <Leave> "::deken::treeresults::leaveevent %W" bind ${treeid} <Double-ButtonRelease-1> "::deken::treeresults::doubleclick %W %x %y" proc ::deken::show_results {resultsid} { ::deken::treeresults::show ${resultsid}} proc ::deken::clear_results {resultsid} { ::deken::treeresults::clear ${resultsid}} proc ::deken::clear_selection {resultsid} { ::deken::treeresults::clear_selection ${resultsid} } scrollbar ${winid}.tab.results.ys -orient vertical -command "${winid}.tab.results yview" pack ${winid}.tab.results.ys -side right -fill "y" } ] } { text ${winid}.tab.results -takefocus 0 -cursor hand2 -height 100 -yscrollcommand "${winid}.tab.results.ys set" scrollbar ${winid}.tab.results.ys -orient vertical -command "${winid}.tab.results yview" pack ${winid}.tab.results.ys -side right -fill "y" } ${winid}.tab add ${winid}.tab.results -text [_ "Search Results"] ${winid}.tab add ${winid}.tab.info -text [_ "Log"] ::deken::show_tab ${winid} info variable infoid set resultsid ${winid}.tab.results set infoid ${winid}.tab.info } ] } { text ${winid}.results -takefocus 0 -cursor hand2 -height 100 -yscrollcommand "${winid}.results.ys set" scrollbar ${winid}.results.ys -orient vertical -command "${winid}.results yview" pack ${winid}.results.ys -side right -fill "y" pack ${winid}.results -side top -padx 6 -pady 3 -fill both -expand true } frame ${winid}.progress pack ${winid}.progress -side top -fill "x" if { ! [ catch { ttk::progressbar ${winid}.progress.bar -orient horizontal -length 640 -maximum 100 -mode determinate -variable ::deken::progressvar } stdout ] } { pack ${winid}.progress.bar -side top -fill "x" proc ::deken::progress {x} { set ::deken::progressvar ${x} } label ${winid}.progress.label -textvariable ::deken::progresstext -padx 0 -borderwidth 0 place ${winid}.progress.label -in ${winid}.progress.bar -x 1 } frame ${winid}.status pack ${winid}.status -side bottom -fill "x" -pady 3 label ${winid}.status.label -textvariable ::deken::statustext -relief sunken -anchor "w" pack ${winid}.status.label -side bottom -fill "x" button ${winid}.status.install -text [_ "Install" ] \ -state disabled \ -command "::deken::menu_installselected ${resultsid}" pack ${winid}.status.install -side right -padx 6 -pady 3 -ipadx 10 } proc ::deken::show_tab {winid tab} { if { [winfo exists ${winid}.tab.${tab}] } { ${winid}.tab select ${winid}.tab.${tab} } } proc ::deken::open_search_xxx {searchtype xxx} { set winid ${::deken::winid} ::deken::open_searchui ${winid} ::deken::clearpost set searchterm {} if { ${::deken::searchtype} eq "${searchtype}" } { append searchterm [${winid}.searchbit.entry get] } if { ${searchterm} ne {} } { append searchterm " " } foreach xx ${xxx} { foreach x ${xx} { lappend searchterm ${x} } } ${winid}.searchbit.entry delete 0 end ${winid}.searchbit.entry insert end ${searchterm} set ::deken::searchtype "${searchtype}" ::deken::update_searchbutton ${winid} } proc ::deken::open_search_objects {args} { ::deken::open_search_xxx "objects" ${args} } proc ::deken::open_search_libraries {args} { ::deken::open_search_xxx "libraries" ${args} } proc ::deken::open_search_translations {args} { ::deken::open_search_xxx "translations" ${args} } proc ::deken::open_search_missing_libraries {args} { # LATER this should only display not-installed libraries ::deken::open_search_xxx "libraries" ${args} } proc ::deken::initiate_search {winid} { set searchterm [${winid}.searchbit.entry get] # let the user know what we're doing ::deken::show_tab ${winid} info ::deken::clearpost ::deken::statuspost [format [_ "Searching for \"%s\"..." ] ${searchterm} ] set ::deken::progressvar 0 ::deken::progressstatus "" if { [ catch { set results [::deken::search_for ${searchterm}] } stdout ] } { ::deken::utilities::debug [format [_ "online? %s" ] ${stdout} ] ::deken::statuspost [_ "Unable to perform search. Are you online?" ] error } else { # delete all text in the results variable resultsid ::deken::clear_results ${resultsid} set ::deken::selected {} set ::deken::results ${results} set matchcount 0 foreach r ${results} { foreach {_ _ match} ${r} {break} if { ${match} } { incr matchcount } } if {[llength ${results}] != 0} { ::deken::show_results ${resultsid} set msg [format [_ "Found %1\$d usable packages (of %2\$d packages in total)." ] ${matchcount} [llength ${results}]] ::deken::statuspost [format {"%s": %s} ${searchterm} ${msg}] if { ${matchcount} } { ::deken::show_tab ${winid} results } else { ::deken::post [_ "It appears that there are no matching packages for your architecture." ] warn } } else { ::deken::statuspost [_ "No matching externals found." ] set msg [_ "Try using the full name e.g. 'freeverb~'." ] ::deken::post " ${msg}" set msg [_ "Or use wildcards like 'freeverb*'." ] ::deken::post " ${msg}" } } } ## deken::textresults: show versions of libraries in a simple text widget namespace eval ::deken::textresults:: { } # display a single found entry in a simple text widget proc ::deken::textresults::show_result {resultsid counter result showmatches} { foreach {title cmd match comment status contextcmd pkgname} ${result} {break} set tag ch${counter} set tags [list ${tag} [expr {${match}?"archmatch":"noarchmatch"} ] ] if { "${pkgname}" ne "" } {lappend tags "/${pkgname}"} if {(${match} == ${showmatches})} { set comment [string map {"\n" "\n\t"} ${comment}] ::deken::post_result "${title}\n\t${comment}\n" ${tags} ::deken::highlightable_resulttag ${tag} ::deken::bind_resulttag ${tag} <Enter> "+::deken::status {${status}}" ::deken::bind_resulttag ${tag} <1> "${cmd}" if { "" ne ${contextcmd} } { ::deken::bind_contextmenu ${resultsid} ${tag} ${contextcmd} } } } # display all found entries in a simple text widget proc ::deken::textresults::show {resultsid} { set counter 0 # build the list UI of results foreach r ${::deken::results} { ::deken::textresults::show_result ${resultsid} ${counter} ${r} 1 incr counter } if { "${::deken::hideforeignarch}" } { # skip display of non-matching archs } else { set counter 0 foreach r ${::deken::results} { ::deken::textresults::show_result ${resultsid} ${counter} ${r} 0 incr counter } } if { [winfo exists ${resultsid}] } { ${resultsid} see 0.0 } } proc ::deken::textresults::clear {resultsid} { if { [winfo exists ${resultsid}] } { ${resultsid} delete 1.0 end } } proc ::deken::textresults::selectpackage {resultsid pkgname installcmd} { # set/unset the selection in a "dict" set state {} set counter 1 foreach {k v} ${::deken::selected} { if { ${k} eq ${pkgname} } { if { ${v} ne ${installcmd} } { set state 1 lset ::deken::selected ${counter} ${installcmd} } else { set state 0 lset ::deken::selected ${counter} {} } break } incr counter 2 } if { ${state} eq {} } { # not found in the dict; just add it lappend ::deken::selected ${pkgname} ${installcmd} set state 1 } # set/unset the visual representation (via tags) set counter 0 foreach {a b} [${resultsid} tag ranges /${pkgname}] {${resultsid} tag remove sel ${a} ${b}} if { ${state} } { foreach r ${::deken::results} { if { [lindex ${r} 1] eq ${installcmd} } { foreach {a b} [${resultsid} tag ranges ch${counter}] {${resultsid} tag add sel ${a} ${b}} } incr counter } } ::deken::update_installbutton [winfo toplevel ${resultsid}] } proc ::deken::textresults::clear_selection {resultsid} { if { [winfo exists ${resultsid}] } { foreach {a b} [${resultsid} tag ranges sel] {${resultsid} tag remove sel ${a} ${b}} } } ## deken::treeresults: show versions of libraries in a tree-view # TASKs # - each library (distinguished by name) is a separate (expandable/collapsible) node # - expanding a library node shows all versions # - the library node shows which version of the library is going to be installed (if any) # - the tree can be sorted in both directions by clicking on any of the headings # SELECTING which library to install # - clicking on a version # - if the version was currently selected for installation, it is now deselected # - otherwise select this version to be installed # - clicking on a library node # - if no version of the given library has been selected, this selects the most recent compatible version # - otherwise the library is deselected from installation # - multiple selections # - ideally we would just forbid ctrl-clicking for multiple selections # - otherwise, this would select the the most recent compatible version # # CAVEATs # - interaccting with the selection for library 'x' should not interfere with the selection of library 'y' # - incompatible archs should be marked somehow # - incompatible archs must always be explicitly selected # - TODO: what about multi-selecting incompatible archs of only a single library? # - TODO: what about multi-selecting a couple of libraries where some only have incompatible archs? namespace eval ::deken::treeresults:: { } array set ::deken::treeresults::colsort {} array set ::deken::treeresults::skipclick {} array set ::deken::treeresults::activecell {} proc ::deken::treeresults::columnsort {treeid {col "#0"}} { # do we want to sort increasing or decreasing? variable colsort if {! [info exists colsort(${col}) ] } { set colsort(${col}) 1 } set colsort(${col}) [expr { ! $colsort(${col}) }] set dir -increasing if { $colsort(${col}) } { set dir -decreasing } # do the actual sorting if { ${col} eq "#0" } { set sortable {} foreach lib [${treeid} children {}] { lappend sortable [list [${treeid} item ${lib} -text] ${lib}] } set pkgs {} foreach x [lsort -nocase ${dir} -index 0 ${sortable}] { lappend pkgs [lindex ${x} 1] } ${treeid} children {} ${pkgs} } else { foreach lib [${treeid} children {}] { set sortable {} foreach pkg [${treeid} children ${lib}] { lappend sortable [list [${treeid} set ${pkg} ${col}] ${pkg}] } set pkgs {} foreach x [lsort -nocase ${dir} -index 0 -command ::deken::versioncompare ${sortable}] { lappend pkgs [lindex ${x} 1] } ${treeid} children ${lib} ${pkgs} } } ## add some decoration to the header indicating the sort-direction set label_incr "\u2b07" set label_decr "\u2b06" set dirsym "${label_incr}" if { ${dir} eq "-decreasing" } { set dirsym "${label_decr}" } # clear all the increasing/decreasing indicators from the headings foreach c [${treeid} cget -columns] { ${treeid} heading ${c} -text [regsub "(${label_decr}|${label_incr})$" [${treeid} heading ${c} -text] {}] } set c "#0" ${treeid} heading ${c} -text [regsub "(${label_decr}|${label_incr})$" [${treeid} heading ${c} -text] {}] # and finally set the increasing/decreasing indicator for the sorted column ${treeid} heading ${col} -text [${treeid} heading ${col} -text]${dirsym} } proc ::deken::treeresults::focusbyindex {treeid index} { # make sure that the entry <index> is visible ${treeid} yview ${index} } proc ::deken::treeresults::getselected {treeid} { set sel {} foreach id [${treeid} children {}] { set data [${treeid} item ${id} -values] if { "${data}" eq {} } { continue } lappend sel [linsert ${data} 0 [${treeid} item ${id} -text]] } return ${sel} } proc ::deken::treeresults::selection_skip {treeid {state 1}} { # expanding/collapsing a node results in a selection message # so we set a flag to skip it variable skipclick if { ! [info exists skipclick(${treeid})] } { set skipclick(${treeid}) 0 } set skip $skipclick(${treeid}) set skipclick(${treeid}) ${state} return ${skip} } proc ::deken::treeresults::selection_changed {treeid} { if { [::deken::treeresults::selection_skip ${treeid} 0] } { return } ${treeid} tag remove selpkg foreach sel [${treeid} selection] { set lib [${treeid} parent ${sel}] if { ${lib} eq {} } { # library node set lib ${sel} if { [${treeid} item ${sel} -values] eq {} } { # currently no data, find the best match! set children {} foreach child [${treeid} children ${lib}] { set data [${treeid} item ${child} -values] if {[lindex ${data} 4]} { lappend children [list [lindex ${data} 0] ${child}] } } set children [lsort -decreasing -index 0 -command ::deken::versioncompare ${children}] set sel {} foreach child ${children} { foreach {version sel} ${child} {break} break } if { ${sel} != {}} { ${treeid} item ${lib} -values [${treeid} item ${sel} -values] ${treeid} tag add selpkg ${sel} } } else { ${treeid} item ${lib} -values {} } } else { # package (leaf) set data [${treeid} item ${sel} -values] if { ${data} eq [${treeid} item ${lib} -values] } { # we were already selected, so deselect us ${treeid} item ${lib} -values {} } else { ${treeid} item ${lib} -values ${data} ${treeid} tag add selpkg ${sel} } } } ## fixup the selection set bound [bind ${treeid} <<TreeviewSelect>>] bind ${treeid} <<TreeviewSelect>> {} # unselect the old ones, and select the new ones ${treeid} selection remove [${treeid} selection] set counter 0 set ::deken::selected {} foreach id [${treeid} children {}] { set data [${treeid} item ${id} -values] if { ${data} eq {} } { continue } ${treeid} selection add ${id} lappend ::deken::selected [${treeid} item ${id} -text] [lindex ${data} 5] } ::deken::update_installbutton [winfo toplevel ${treeid}] after idle "bind ${treeid} <<TreeviewSelect>> \{${bound}\}" } proc ::deken::treeresults::presorter {A B} { # <a>, <b>: [<pkgname> <version> <title> <uploader> <timestamp> <match> <cmd> <contextcmd>] # compare to library lists: <pkgname> (ascending), <match> (descending), <version> (descending), <date> (descending) foreach {a_name a_ver _ _ a_time a_match} ${A} {break} foreach {b_name b_ver _ _ b_time b_match} ${B} {break} if {${a_name} < ${b_name}} { return 1 } elseif {${a_name} > ${b_name}} { return -1 } if {${a_match} < ${b_match}} { return -1 } elseif {${a_match} > ${b_match}} { return 1 } set v [::deken::versioncompare ${a_ver} ${b_ver}] if { ${v} != "0" } {return ${v}} if {${a_time} < ${b_time}} { return -1 } elseif {${a_time} > ${b_time}} { return 1 } return 0 } proc ::deken::treeresults::motionevent {treeid x y} { set item [${treeid} identify item ${x} ${y}] set data [${treeid} item ${item} -values] if {! [info exists ::deken::treeresults::activecell(${treeid}) ] } { set ::deken::treeresults::activecell(${treeid}) {} } set title [lindex ${data} 1] set status [lindex ${data} 7] set subtitle [lindex ${data} 8] # the status bar if { "${status}" != "" } { ::deken::status ${status} } # the balloon if { $::deken::treeresults::activecell(${treeid}) != ${item} } { set ::deken::treeresults::activecell(${treeid}) ${item} set X [expr {[winfo rootx ${treeid}] + 10}] set Y [expr {[winfo rooty ${treeid}] + ${y} + 10}] ::deken::balloon::show ${treeid}_balloon ${X} ${Y} [string trim "${title}\n${subtitle}"] } } proc ::deken::treeresults::leaveevent {treeid} { set ::deken::treeresults::activecell(${treeid}) {} ::deken::balloon::hide ${treeid}_balloon } proc ::deken::treeresults::doubleclick {treeid x y} { set item [${treeid} identify item ${x} ${y}] set installitem ${item} if { [${treeid} bbox ${item}] eq {} } { set installitem {} } if { ${installitem} eq {} } { # the user double-clicked on a column heading set column [${treeid} identify column ${x} ${y}] if { ${column} eq "#0" } { # we don't want to sort by column#0 # instead we open/close the items set have_open 0 set have_close 0 foreach lib [${treeid} children {}] { if { [${treeid} item ${lib} -open] } { incr have_open } else { incr have_close } } set do_open [expr {${have_close} > ${have_open}}] foreach lib [${treeid} children {}] { ${treeid} item ${lib} -open ${do_open} } return } set column [${treeid} column ${column} -id] # do we want to sort increasing or decreasing? variable colsort if {! [info exists colsort(${column}) ] } { set colsort(${column}) 1 } set dir -increasing if { $colsort(${column}) } { set dir -decreasing } set sortable {} foreach lib [${treeid} children {}] { foreach pkg [${treeid} children ${lib}] { lappend sortable [list [${treeid} set ${pkg} ${column}] ${lib}] break } } set pkgs {} foreach x [lsort -nocase ${dir} -index 0 -command ::deken::versioncompare ${sortable}] { lappend pkgs [lindex ${x} 1] } ${treeid} children {} ${pkgs} if { ${item} eq {} } { # yikes: if we are not scrolled down, the double-click will trigger columnsort twice # so we do an extra round here... ::deken::treeresults::columnsort ${treeid} ${column} } return } set data [${treeid} item ${item} -values] set cmd [lindex ${data} 5] if { ${cmd} != "" } { ::deken::post "" eval ${cmd} } } proc ::deken::treeresults::show {treeid} { # shown: library, version, title, uploader, date set libraries {} foreach r ${::deken::results} { foreach {title cmd match subtitle statusline contextcmd pkgname version uploader timestamp} ${r} {break} if { "${::deken::hideforeignarch}" } { if { ! ${match} } { continue } } lappend libraries [list ${pkgname} ${version} ${title} ${uploader} ${timestamp} ${match} ${cmd} ${contextcmd} ${statusline} ${subtitle}] } # sort the libraries set libraries [lsort -decreasing -command ::deken::treeresults::presorter ${libraries}] set lastlib {} set index {} ##foreach v {"#0" version title uploader date} { ## set width(${v}) 0 ##} #puts [time { foreach lib ${libraries} { set l [lindex ${lib} 0] set data [lrange ${lib} 1 end] if {${l} ne ${lastlib}} { set lastlib ${l} set index [${treeid} insert {} end -text ${l} -open 0 -tags {library}] ##set w [font measure {-underline false} -displayof ${treeid} ${l}] ##if {${w} > $width(#0)} {set width(#0) ${w}} } set archtag noarchmatch if { [lindex ${lib} 5] } { set archtag archmatch } set x [${treeid} insert ${index} end -values ${data} -tags [list package ${archtag}]] ##set vidx 0 ##foreach v {version title uploader date} { ## set w [font measure {-underline false} -displayof ${treeid} [lindex ${data} ${vidx}]] ## incr vidx ## if { ${w} > $width(${v}) } {set width($v) ${w} } ##} ${treeid} tag add ${x} ${x} ::deken::bind_contextmenu ${treeid} ${x} [lindex ${lib} 7] } #}] ### setting the width as a few caveats ## - we end up with cut off texts anyhow ## (i guess this is mostly a problem with requiring too much text) ## - the widths don#t get fully applied automatically ## (as soon as you drag one of the column delimiters, the other snap into place) ## - it takes forever. ## (simply calculating the required widths for 148 entries takes ~7200ms, ## as opposed to ~1.2ms for not calculating them) ## #foreach v {version title uploader date} { # incr width(${v}) 10 # ${treeid} column ${v} -width $width(${v}) #} } proc ::deken::treeresults::clear {resultsid} { ${resultsid} delete [${resultsid} children {}] } proc ::deken::treeresults::clear_selection {treeid} { if { ! [winfo exists ${treeid}] } { return } set bound [bind ${treeid} <<TreeviewSelect>>] bind ${treeid} <<TreeviewSelect>> {} # unselect the old ones, and select the new ones ${treeid} selection remove [${treeid} selection] ${treeid} tag remove selpkg foreach item [${treeid} children {}] { ${treeid} item ${item} -values {} } after idle "bind ${treeid} <<TreeviewSelect>> \{${bound}\}" } ######################################################## proc ::deken::show_results {resultsid} { ::deken::textresults::show ${resultsid} } proc ::deken::clear_results {resultsid} { ::deken::textresults::clear ${resultsid} } proc ::deken::clear_selection {resultsid} { ::deken::textresults::clear_selection ${resultsid} } ######################################################## ## tooltips # code based on example by Donal Fellows in 2001 # https://groups.google.com/g/comp.lang.tcl/c/IhNlXBxL1_I/m/sF4sNhpi7XQJ namespace eval ::deken::balloon { } proc ::deken::balloon::show {winid x y msg {x_offset 0} {y_offset 0}} { set ::deken::balloon::message(${winid}) ${msg} if {![winfo exist ${winid}]} { toplevel ${winid} wm overrideredirect ${winid} 1 label ${winid}.label \ -highlightthick 0 -relief solid -borderwidth 1 \ -textvariable ::deken::balloon::message(${winid}) pack ${winid}.label -expand 1 -fill x } if { ${msg} == {} } { wm withdraw ${winid} return } set g [format +%d+%d [expr {${x} + ${x_offset}}] [expr {${y} + ${y_offset}}]] # This is probably overdoing it, but better too much than too little wm geometry ${winid} ${g} wm deiconify ${winid} wm geometry ${winid} ${g} raise ${winid} after idle "[list wm geometry ${winid} ${g}]; raise ${winid}" } proc ::deken::balloon::hide {winid} { if {[winfo exist ${winid}]} { wm withdraw ${winid} } } ######################################################## proc ::deken::ask_installdir {{installdir ""} {extname ""}} { while {1} { if { "${installdir}" == "" } { set result [tk_messageBox \ -message [_ "Please select a (writable) installation directory!"] \ -icon warning -type retrycancel -default retry \ -parent ${::deken::winid}] switch -- "${result}" { cancel {return} retry { if {[::deken::prompt_installdir]} { set installdir ${::deken::installpath} } else { continue } } } } else { set result [tk_messageBox \ -message [format [_ "Install %1\$s to %2\$s?" ] ${extname} ${installdir}] \ -icon question -type yesnocancel -default yes \ -parent ${::deken::winid}] switch -- "${result}" { cancel {return} yes { } no { set prevpath ${::deken::installpath} if {[::deken::prompt_installdir]} { set keepprevpath 1 set installdir ${::deken::installpath} # if docsdir is set & the install path is valid, # saying "no" is temporary to ensure the docsdir # hierarchy remains, use the Path dialog to override if {[namespace exists ::pd_docsdir] && [::pd_docsdir::path_is_valid] && [file writable [file normalize ${prevpath}]] } { set keepprevpath 0 } if {${keepprevpath}} { set ::deken::installpath ${prevpath} } } else { continue } } } } if { "${installdir}" != "" } { # try creating the installdir...just in case catch { file mkdir ${installdir} } } # check whether this is a writable directory set installdir [ ::deken::utilities::get_writabledir [list ${installdir} ] ] if { "${installdir}" != "" } { # stop looping if we've found our dir break } } return ${installdir} } proc ::deken::ensure_installdir {{installdir ""} {extname ""}} { ## make sure that the destination path exists ### if ::deken::installpath is set, use the first writable item ### if not, get a writable item from one of the searchpaths ### if this still doesn't help, ask the user if { "${installdir}" != "" } {return ${installdir}} set installdir [::deken::find_installpath] if { "${installdir}" != "" } {return ${installdir}} if {[namespace exists ::pd_docsdir] && [::pd_docsdir::externals_path_is_valid]} { # if the docspath is set, try the externals subdir set installdir [::pd_docsdir::get_externals_path] } if { "${installdir}" != "" } {return ${installdir}} # ask the user (and remember the decision) ::deken::prompt_installdir set installdir [ ::deken::utilities::get_writabledir [list ${::deken::installpath} ] ] return [::deken::ask_installdir [::deken::utilities::expandpath ${installdir} ] ${extname}] } # handle a clicked link proc ::deken::install_link {URL filename} { ## make sure that the destination path exists ### if ::deken::installpath is set, use the first writable item ### if not, get a writable item from one of the searchpaths ### if this still doesn't help, ask the user variable winid set installbutton ${winid}.status.install if {[winfo exists ${installbutton}]} { ${installbutton} configure -state disabled } ::deken::show_tab ${winid} info set installdir [::deken::ensure_installdir "" ${filename}] if { "${installdir}" == "" } { ::deken::utilities::debug [format [_ "Cancelling download of '%s': No installation directory given." ] ${filename}] ::deken::statuspost [format [_ "Installing to non-existent directory failed" ] ${filename}] error return } if { ! [file exists ${installdir}] } { catch { file mkdir ${installdir} } } if { ! [file isdirectory ${installdir}] } { set msg [_ "Directory does not exist!" ] ::deken::post [format [_ "Unable to install to '%s'" ] ${installdir} ] error ::deken::post "\t${msg}" error set installdir [::deken::do_prompt_installdir ${installdir}] if { "${installdir}" == "" } { #::deken::update_installbutton ${winid} return } } if { ! [file isdirectory ${installdir}] } { ::deken::post [format [_ "Unable to install to '%s'" ] ${installdir} ] error set msg [_ "Directory does not exist!" ] ::deken::post "\t${msg}" error return } if { [::deken::utilities::get_writabledir [list ${installdir}]] == "" } { ::deken::post [format [_ "Unable to install to '%s'" ] ${installdir} ] error set msg [_ "Directory is not writable!" ] ::deken::post "\t${msg}" error return } set parsedfilename [::deken::utilities::parse_filename ${filename}] set fullpkgfile [::deken::utilities::get_tmpfilename ${installdir} [::deken::utilities::get_filenameextension ${filename}] "[lindex ${parsedfilename} 0]\[[lindex ${parsedfilename} 1]\]" ] ::deken::statuspost [format [_ "Downloading '%s'" ] ${filename}] info 0 ::deken::utilities::debug [format [_ "Commencing download of '%1\$s' into '%2\$s'..." ] ${URL} ${installdir}] ::deken::syncgui set fullpkgfile [::deken::utilities::download_file ${URL} ${fullpkgfile} "::deken::download_progress"] if { "${fullpkgfile}" eq "" } { ::deken::utilities::debug [_ "aborting."] ::deken::statuspost [format [_ "Downloading '%s' failed" ] ${filename}] error ::deken::progressstatus [_ "Download failed!" ] ::deken::progress 0 return } set msg [_ "Download completed! Verifying..." ] ::deken::progressstatus ${msg} ::deken::post "${msg}" info set result [::deken::verify_sha256_gui ${URL} ${fullpkgfile}] if { ! ${result} } { # verification failed if { ! "${::deken::keep_package}" } { catch { file delete ${fullpkgfile} } } ::deken::progress 0 return } if { ${result} < 0 } { # verification failed, but we ignore it if { ${result} > -10 } { ::deken::statuspost [_ "Ignoring checksum mismatch" ] info 0 } elseif { ${result} > -100 } { ::deken::statuspost [_ "Ignoring checksum errors" ] info 0 } } ::deken::install_package ${fullpkgfile} ${filename} ${installdir} ${::deken::keep_package} ::deken::update_installbutton ${winid} } # print the download progress to the results window proc ::deken::download_progress {token total current} { if { ${total} > 0 } { ::deken::progress [expr {round(100 * (1.0 * ${current} / ${total}))}] } } # test for platform match with our current platform proc ::deken::architecture_match {archs} { if { "translations" eq "${::deken::searchtype}" } { foreach arch ${archs} { if { "i18n" eq "${arch}" } { return 1 } if {[string match "i18n-*" ${arch}] } { return 1 } } return 0 } # if there are no architecture sections this must be arch-independent if { ! [llength ${archs}] } { return 1} set OS "$::deken::platform(os)" set MACHINE "$::deken::platform(machine)" set BITS "$::deken::platform(bits)" set FLOATSIZE "$::deken::platform(floatsize)" if { "${::deken::userplatform}" != "" } { ## FIXXME what if the user-supplied input isn't valid? regexp -- {(.*)-(.*)-(.*)} ${::deken::userplatform} _ OS MACHINE FLOATSIZE } # strip the little-endian indicator from arm-archs, it's the default regexp -- {(armv[0-9]*)[lL]} ${MACHINE} _ MACHINE set MACHINE [string tolower ${MACHINE}] set OS [string tolower ${OS}] # check each architecture in our list against the current one foreach arch ${archs} { if { [ regexp -- {(.*)-(.*)-(.*)} ${arch} _ os machine floatsize ] } { # normalize arm-architectures by stripping away sub-architectures # TODO: leave any big-endian indicator in place regexp -- {(armv[0-9]*)[^0-9]*} ${machine} _ machine set machine [string tolower ${machine}] set os [string tolower ${os}] if { ("${os}" eq "${OS}") && (("${floatsize}" eq "${FLOATSIZE}") || ("${floatsize}" eq "0"))} { ## so OS and floatsize match... ## check whether the CPU matches as well if { "${machine}" eq "${MACHINE}" } {return 1} ## not exactly; see whether it is in the list of compat CPUs if {[llength [array names ::deken::architecture_substitutes -exact "${MACHINE}"]]} { foreach cpu $::deken::architecture_substitutes(${MACHINE}) { if { "${machine}" eq "${cpu}" } {return 1} } } } } } return 0 } proc ::deken::search_for {term} { set result [list] foreach searcher ${::deken::backends} { if {[catch { foreach r [ ${searcher} ${term} ] { if { "" eq [lindex ${r} 0] } { # data is already normalized } else { # legacy data format foreach {title cmd match comment status} ${r} {break} set r [::deken::normalize_result ${title} ${cmd} ${match} ${comment} ${status}] } lappend result [lrange ${r} 1 end] } } stdout] } { ::deken::utilities::debug "${searcher:} ${stdout}" } } return ${result} } proc ::deken::initialize {} { # console message to let them know we're loaded ## but only if we are being called as a plugin (not as built-in) if { "" != "${::current_plugin_loadpath}" } { ::pdwindow::debug [format [_ "\[deken\] deken-plugin.tcl (Pd externals search) loaded from %s." ] ${::current_plugin_loadpath} ] ::pdwindow::debug "\n" } set msg [format [_ "\[deken\] Platform detected: %s" ] [::deken::platform2string 1] ] ::pdwindow::verbose 0 "${msg}\n" # try to set install path when plugin is loaded set ::deken::installpath [::deken::find_installpath] # create an entry for our search in the menu (or reuse an existing one) # if there's a 'tools' menu, use that, otherwise use the 'help' menu set mymenu .menubar.tools if { [winfo exists ${mymenu}]} { # we got Tools->, so if there's Help->Find externals... entry, drop it catch { .menubar.help delete [_ "Find externals"] } } else { set mymenu .menubar.help } if { [catch { # if there's already an entry, make sure to use our 'open_searchui' rather than the built-in ${mymenu} entryconfigure [_ "Find externals"] -command {::deken::open_searchui ${::deken::winid}} } _ ] } { # otherwise create a new menu entry if { ${mymenu} eq ".menubar.help" } { ${mymenu} add separator } ${mymenu} add command -label [_ "Find externals"] -command {::deken::open_searchui ${::deken::winid}} } # bind all <${::modifier}-Key-s> {::deken::open_helpbrowser .helpbrowser2} } # ###################################################################### # ################ search backends ##################################### # ###################################################################### proc ::deken::register {fun} { # register a searchfunction with deken. # the searchfunction <fun> will be called with a <searchterm>, # and must return a list of <result>. # <searchterm> is a list of (whitespace separated) words. # each word denotes a library or library-object to search for and may # contain wildcards ("*"). # the <result> should be normalized via ::deken::search::normalize_result # failing to do so, a <result> is a list <name> <cmd> <match> <comment> <status> <args...> # - <title> non-empty name of the library (to be shown to the user as search-result) # - <cmd> the full command to run to install the library # - <match> boolean value to indicate whether this entry matches the current architecture # - <subtitle> additional text to be shown under the <name> # - <status> additional text to be shown in the STATUS line if the mouse hovers over the result # - <args>... additional args (ignored) # the library <name> must be non-empty (and empty value is reserved for normalized results) set ::deken::backends [linsert ${::deken::backends} 0 ${fun}] } ## API draft # each backend is implemented via a single proc ## that takes a single argument "term", the term to search for ## an empty term indicates "search for all" # the backend then returns a list of results ## each result is a list of the following elements: ## <title> <cmd> <match> <comment> <status> ## title: the primary name to display ## (the user will select the element by this name) ## e.g. "frobscottle-1.10 (Linux/amd64)" ## cmd : a command that will install the selected library ## e.g. "[list ::deken::install_link http://bfg.org/frobscottle-1.10.zip frobscottle-1.10.zip]" ## match: an integer indicating whether this entry is actually usable ## on this host (1) or not (0) ## comment: secondary line to display ## e.g. "uploaded by the BFG in 1982" ## status: line to display in the status-line ## e.g. "http://bfg.org/frobscottle-1.10.zip" # note on sorting: ## the results ought to be sorted with most up-to-date first ## (filtering based on architecture-matches should be ignored when sorting!) # note on helper-functions: ## you can put whatever you like into <cmd>, even your own proc # registration ## to register a new search function, call `::deken::register ${myfun}` # namespace ## you are welcome to use the ::deken::search:: namespace ## #################################################################### ## searching puredata.info namespace eval ::deken::search::dekenserver { # deken servers to use variable url_primary variable urls_secondary variable urls_ephemeral # should we actually use them? variable use_url_primary variable use_urls_secondary variable use_urls_ephemeral } # the main deken-url ::deken::utilities::setdefault ::deken::search::dekenserver::use_url_primary 1 set ::deken::search::dekenserver::url_primary "http://deken.puredata.info/search" if { ! [catch {package present tls} stdout] } { set ::deken::search::dekenserver::url_primary "https://deken.puredata.info/search" } catch {set ::deken::search::dekenserver::url_primary $::env(DEKENSERVER)} catch {set ::deken::search::dekenserver::url_primary $::env(DEKEN_SEARCH_URL)} # additional (fixed) deken-servers ::deken::utilities::setdefault ::deken::search::dekenserver::use_urls_secondary 0 ::deken::utilities::setdefault ::deken::search::dekenserver::urls_secondary {} # additional (ephemeral) deken-servers ::deken::utilities::setdefault ::deken::search::dekenserver::use_urls_ephemeral 0 ## those we expect ::deken::utilities::setdefault ::deken::search::dekenserver::urls_ephemeral {} ## those that are there array set ::deken::search::dekenserver::urls_ephemeral_existing {} proc ::deken::search::dekenserver::search {term} { set tmpurls {} foreach {k v} [array get ::deken::search::dekenserver::urls_ephemeral_existing] { lappend tmpurls ${v} } # all the search URLs set urls {} if { ${::deken::search::dekenserver::use_url_primary} } { lappend urls ${::deken::search::dekenserver::url_primary} } if { ${::deken::search::dekenserver::use_urls_secondary} } { set urls [concat ${urls} ${::deken::search::dekenserver::urls_secondary}] } if { ${::deken::search::dekenserver::use_urls_ephemeral} } { set urls [concat \ ${urls} \ [::deken::utilities::lists_intersect ${::deken::search::dekenserver::urls_ephemeral} ${tmpurls}] \ ] } # remove duplicate entries set urls [::deken::utilities::list_unique ${urls}] # search all the urls array set results {} set urlcount 0 foreach s ${urls} { # skip empty urls if { ${s} eq {} } { continue } ::deken::post [format [_ "Searching on %s..."] ${s} ] debug set resultcount 0 # get the results from the given url, and add them to our results set foreach r [::deken::search::dekenserver::search_server ${term} ${s}] { set results(${r}) {} incr resultcount } ::deken::post [format [_ "Searching on %1\$s returned %2\$d results"] ${s} ${resultcount}] debug incr urlcount } if { ${urlcount} == 0 } { ::deken::post [format [_ "No usable servers for searching found..."] ${urls} ] debug } set splitCont [array names results] if { [llength ${splitCont}] == 0 } { return ${splitCont} } set searchresults [list] # loop through the resulting tab-delimited table if { [catch { set latestrelease0 [dict create] set latestrelease1 [dict create] set newestversion [dict create] foreach ele ${splitCont} { set ele [ string trim ${ele} ] if { "" ne ${ele} } { foreach {name URL creator date} [ split ${ele} "\t" ] {break} set filename [ file tail ${URL} ] foreach {pkgname version archs} [ ::deken::utilities::parse_filename ${filename} ] {break} #if { ${version} eq "0.0.extended" } { set date "0000-00-00 00:02:00" } set olddate {} set match [::deken::architecture_match "${archs}" ] if { ${match} } { catch { set olddate [dict get ${latestrelease1} ${pkgname}] } set oldversion {} catch { set oldversion [dict get ${newestversion} ${pkgname}]} if { [::deken::versioncompare ${version} ${oldversion}] > 0 } { dict set newestversion ${pkgname} ${version} } } else { catch { set olddate [dict get ${latestrelease0} ${pkgname}] } } if { ${date} > ${olddate} } { dict set latestrelease${match} ${pkgname} ${date} } } } } stdout ] } { set latestrelease0 {} set latestrelease1 {} set newestversion {} } set vsep "\u0001" foreach ele ${splitCont} { set ele [ string trim ${ele} ] if { "" ne ${ele} } { foreach {name URL creator date} [ split ${ele} "\t" ] {break} set decURL [::deken::utilities::urldecode ${URL}] set filename [ file tail ${URL} ] set cmd [list ::deken::install_link ${decURL} ${filename}] set pkgverarch [ ::deken::utilities::parse_filename ${filename} ] set pkgname [lindex ${pkgverarch} 0] set version [lindex ${pkgverarch} 1] set archs [lindex ${pkgverarch} 2] set match [::deken::architecture_match "${archs}" ] set comment [format [_ "Uploaded by %1\$s @ %2\$s" ] ${creator} ${date} ] set status ${URL} set sortprefix "0000-00-00 00:01:00" if { ${match} == 0 } { catch { set sortprefix [dict get ${latestrelease0} ${pkgname}] } } else { if { "${::deken::hideoldversions}" } { # If this version is not the newest one, mark it as unmatched catch { set oldversion [dict get ${newestversion} ${pkgname}] if { [::deken::versioncompare ${version} ${oldversion}] < 0 } { set match 0 } } } } catch { set sortprefix [dict get ${latestrelease1} ${pkgname}] } # the ${vsep} should sort before all other characters that might appear in version strings, # as it unsures that "1.2" sorts before "1.2-1" # the space (or some other character that sorts after "\t") after the ${version} is important, # as it ensures that "0.2~1" sorts before "1.2" set sortname "${sortprefix}${vsep}${pkgname}${vsep}${version} ${vsep}${date}" set contextcmd [list ::deken::search::dekenserver::contextmenu %W %x %y ${pkgname} ${URL}] set res [list ${sortname} ${filename} ${name} ${cmd} ${match} ${comment} ${status} ${contextcmd} ${pkgname} ${version} ${creator} ${date}] lappend searchresults ${res} } } set sortedresult [] foreach r [lsort -command ::deken::versioncompare -decreasing -index 0 ${searchresults} ] { foreach {sortname filename title cmd match comment status menus pkgname version creator date} ${r} { lappend sortedresult [::deken::normalize_result ${title} ${cmd} ${match} ${comment} ${status} ${menus} ${pkgname} ${version} ${creator} ${date}] break } } return ${sortedresult} } proc ::deken::search::dekenserver::search_server {term dekenurl} { set queryterm {} if { ${::deken::searchtype} eq "translations" && ${term} eq "" } { # special handling of searching for all translations (so we ONLY get translations) set term {*} } foreach x ${term} {lappend queryterm ${::deken::searchtype} ${x}} if { [ catch {set queryterm [::http::formatQuery {*}${queryterm} ] } stdout ] } { set queryterm [ join ${term} "&${::deken::searchtype}=" ] set queryterm "${::deken::searchtype}=${queryterm}" } # deken-specific socket config set httpaccept [::http::config -accept] set httpagent [::deken::utilities::httpuseragent] ::http::config -accept text/tab-separated-values # fetch search result if { [catch { set token [::http::geturl "${dekenurl}?${queryterm}"] } stdout ] } { set msg [format [_ "Searching for '%s' failed!" ] ${term} ] tk_messageBox \ -title [_ "Search failed" ] \ -message "${msg}\n(${dekenurl})\n${stdout}" \ -icon error -type ok \ -parent ${::deken::winid} return } # restore http settings ::http::config -accept ${httpaccept} ::http::config -useragent ${httpagent} set ncode [::http::ncode ${token}] if { ${ncode} != 200 } { set err [::http::code ${token}] set msg [_ "Unable to perform search."] ::deken::utilities::debug "${msg}\n ${err}" return {} } set contents [::http::data ${token}] ::http::cleanup ${token} return [split ${contents} "\n"] } proc ::deken::search::dekenserver::contextmenu {widget theX theY pkgname URL} { set winid ${::deken::winid} set resultsid ${::deken::resultsid} set with_installmenu 1 catch { # don't show the select/install entries when using a treeview ${resultsid} identify item 0 0 set with_installmenu 0 } set m .dekenresults_contextMenu destroy ${m} menu ${m} set saveURL [string map {"[" "%5B" "]" "%5D"} ${URL}] if { ${with_installmenu} } { set decURL [::deken::utilities::urldecode ${URL}] set filename [ file tail ${URL} ] set pkgverarch [ ::deken::utilities::parse_filename ${filename} ] set pkgname [lindex ${pkgverarch} 0] set cmd [list ::deken::install_link ${decURL} ${filename}] set selcount 0 set selected 0 foreach {k v} ${::deken::selected} { if { ${v} != {} } {incr selcount} if { (${k} eq ${pkgname}) && (${v} eq ${cmd}) } { set selected 1 break } } set msg [_ "Select package for installation" ] if { ${selected} } { set msg [_ "Deselect package" ] } ${m} add command -label "${msg}" -command "::deken::textresults::selectpackage ${resultsid} ${pkgname} {${cmd}}" ${m} add separator } set infoq "url=${URL}" if {$::tcl_platform(platform) ne "windows"} { set infoq [::http::formatQuery url ${URL}] } ${m} add command -label [_ "Open package webpage" ] -command "pd_menucommands::menu_openfile \{https://deken.puredata.info/info?${infoq}\}" ${m} add command -label [_ "Copy package URL" ] -command "clipboard clear; clipboard append ${saveURL}" ${m} add command -label [_ "Copy SHA256 checksum URL" ] -command "clipboard clear; clipboard append ${saveURL}.sha256" ${m} add command -label [_ "Copy OpenGPG signature URL" ] -command "clipboard clear; clipboard append ${saveURL}.asc" set installpath [::deken::find_installpath] if { "${installpath}" ne {} } { if { [file isdir [file join ${installpath} ${pkgname}]] } { ${m} add separator ${m} add command -label [format [_ "Uninstall '%s'" ] ${pkgname}] -command [list ::deken::menu_uninstall_package ${winid} ${pkgname} ${installpath}] } } tk_popup ${m} [expr {[winfo rootx ${widget}] + ${theX}}] [expr {[winfo rooty ${widget}] + ${theY}}] } ::deken::initialize ::deken::register ::deken::search::dekenserver::search } ���������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������deken-0.10.4/deken.gif������������������������������������������������������������������������������0000664�0000000�0000000�00000466204�14764245604�0014467�0����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������GIF89ac1���������~ m  "3.J_/00$$$&:R((((Bb)Js*kW+++,>N-----.^/v0000U2223a3i5555f888899:f:q;;;<<=z=>>>>o>>?y??@ABBBBCCCDoEEEEvEFGGGGIhIJiJKKKLLKtLNNO~vPPPPnQwRRSSSTvTUUUtU|UVWWWX\\\^^^````bccbcdddeedeefffijjjjjmnooooqssstuuuuuuuvvvxxxxzzzz{}}}~퀀邂ꎞ𓓓Õ͜򡡡󪪪ά殮뷷Ӻ! NETSCAPE2.0���! ��,����c���������~ m  "3.J_/00$$$&:R((((Bb)Js*kW+++,>N-----.^/v0000U2223a3i5555f888899:f:q;;;<<=z=>>>>o>>?y??@ABBBBCCCDoEEEEvEFGGGGIhIJiJKKKLLKtLNNO~vPPPPnQwRRSSSTvTUUUtU|UVWWWX\\\^^^````bccbcdddeedeefffijjjjjmnooooqssstuuuuuuuvvvxxxxzzzz{}}}~퀀邂ꎞ𓓓Õ͜򡡡󪪪ά殮뷷Ӻ�H*\ȰÇ#JHŋ3jȱǏ CIɓ(S\ɲ˗0cʜI͛8sɳϟ@ JѣH*]ʴӧPJJիXjʵׯ`ÊKٳhӪ]˶۷pʝKݻx˷߿ LÈ+^̸ǐ#KL˘3k̹ϠCMӨS^ͺװc˞M۸sͻ Nȓ+_μУKNسkνËOӫ_Ͼ˟OϿ�(h& 6F(Vhfv ($h(,0(4h8<@)Diy$L6PF)TViXf\v`)d9hlp)h* n :|矀*蠄j衈&袌6裐F*餔Vj):h馜v6~*j*:hJAI*򊫯뮼k+k&6F+&+.Z[kyb(ڜ+JX`+k�,l'p q{APK&grLǙ �' *0,4l82lH'L̲H??љXtRׂqm3"6#njH$`�ط-tmx|g$I$E.āߐG.WysmЍߟsvb)f!6Hc=׸A r +Lr�o7G/Wo?Sr†8TOl`<f+/υa :ulU|66xWp'0F8 W0 gHPG" |8C$`8HL&"у'<u' A"�0 BM()ÊfLB^₳ @GQD ;f IBL"H@ *�?Ab8_Nz %'.`[6GR6ҕw?`@Z0aIGSRq\ex7dH&2L2t!p4)$<df3nz 8Ir4' RPf^Џ1!̧>~3l&@9 h:LY Mh7T# ]B9KPA eHAzwS?|# bJӚ8ͩNwӞ@ *Pٽ~82bodTJժZXՂHig|#7nUg) pӵ6AEH&t#h.*u~@pG C xd'KZͬf7z%*@ ` hlgKږ ,Xp/*` J6M.r)k6}dWT4~0V`@^A]Dd+8;ypͯ~�,`NЏ~DBkL [ΰ`^NHa8) }޷CJo}8@p!#@D>Z {02L*[Xβ.{`3 !pjk6p\+PLe{$2?ʃFio85HѠiMKCNӦNWVհgMZۺ$ R]@* |pbNVf5SjW{MUg{:`A$X Amq;xܻ5Q[ NO;|[ϸ7y8Ep{5Q<Θh@sP4BDAͧHOҗ;PԧN[XϺַWT.u<`˃ $pۇ xϻO/7;ٞy(7{GOқOWֻgO|OO;ЏO[{ݏOOOȿw8Xx }~8Xh�p H { v!*,؂ǁ04X*8gyy'h>@'DBxHH8X(PRXEX7XKᗅ`bH~VX7h[{=n@hetk}mXz(Vȇw؀y~+@w� (~聓h7'8&��@h�7(~(`7 ~(~~ȋ ��à(PcHIP7I؎GMXE� 8凉GhpH~؋I` P� 7Ǐ8˸~@  0~ ( � s@I ~� Yh029GN~@o� Ћ:Y~ِ PP Y�} �헓; E�GK8 @@)◖JɔW �~�')�g � (dihyوܰ �I 0  ~�� % 淒瘎z Љ0i}� 3W?v"0 P Qi~V "P� Eiz`X)Ziha)~ � � ɖYc0 � IƉʹ))~ 0 ~7 @ ` ؙ)Y癞ٞ & � �i �x&0 ` WB~yyy(J*܀�*~I�` P�)PZ̨ްם^:Z W[e` � Mq*~s:7 ~}  I0 ␠(l*n XJUz �0 z�} CȢ@ p9CګW;xIȌ؏ܰ �LʥX�ت�IY IZJ0~ *_ɦ� "0 }: ɚ*~: -��律ѠlV P *~zj  �c @p, 0ːاT8jo`2 ](xݸI  JI0 Pi*8`�B ٮ(�0;[~VKD�u ~%9 2pP RKc;�+~+�j 乪ע-��+W[2۫4^8NJWZwC)⧫"� קʺD:{ +~ېl �巻Kh *~жe�$Zo 9++pC � c� ,.y@ 9 wO8˓X ` i�@V� @PI < +PNKIxԐ* \۠�8 I9~2�Vp9ȡ *~P�&~` ` u &3뿺 !k=�`ʌT@ E�@ z)�_j~~\Н|'L~ٻ}ǁg7@vǗ ȂY` ' ��JL*Ǥ<` P)~H}Z̢;.9@e0 CƫnX"<kg,ڬܼ  `fF2ΩGxx�Ђ ~ ] дȂ' �@0PШ!"}A8��(-$}&=8 � 0@{XӦqD P  6ʁJF]H]Z=;&P 2oxդf"Q=c֣v�+�| x-z؎~] �~ݓuRءؚᜊP @ ٠  }۸ۺۼ۾=]}ȝʽܽ��@  ȅސ=]} �@ N͆7` @_mV* ]xx,>4^6~- ` � FnψI0N2~PR>M 0 D @0@TH$hblP  � zd^} M^^\ ϐ ~>䓾],~gnꨞꪾb.+N9h>^>FLhw*hj.ʾ쩎� +M @� nTNnFޠߍ n㧾4((P�Iޘ.}^m 0 M ހ�/ EўE P[@a5>]V i0-М` P�Ϟ V-ip^7ގ?OBﭔހ /P�8~ލ �=L^fhO@_߰0J򳠘y �.�� } /b?jen?_[��؊@"o P ӯޱ?o >/A 0 K ޠ: w ?�Q7o�& >QDQF}H%MDRJ-]SL|ԷQΌZQǠEETR910!piPd,oPPv�,o+.+"]ۖBt6%zg^8CX`… S/S7uYdʕwހq oр4 W 28B@,AiV(�,bױ:lh9(_yyXpōL.-KC$A"C]vݽO��(@JP *Xm<�@ 2qG)bι YNYAn; 'B -  7@HB_FѪL_FB6et&?}0Gw,L{-PټyFI-&d,J+2/K/F2ˇ d V"B#x3O=W@ B-Z  pak,oi'# LKiDSO?U*KM�0x?)·۳V[)S; vf+f,NnpÇF)� ɈT2hKP6[msTe f*0D�`EVh^{U(v2>Ė(FQ߾eTd�RHCMbuRf\9e NVp{_|�` 6"D`*1 '8iư[]}HT!&1+cǘ5؁ :(%)*ڇe:o*K7~HMj"Z nv/"zfqF  E4rC5YbYFAA›oWge|1(9 ]_qÆ`;2I*��[m(@ $p'b--<ޛl`:&ac]>>muwRW>S+b5^,F [s� A^(<YR@ aE�+DC"ʎ;8Cw_oFXBށaodxC"fĆ:brЉ9aw!ъIc%6N7bň(KSLFlbe8F:'AceF+N8q@ rÑRZIRq#d&sne(HF *)CJR '#ZҖe.uK[җ#+#JX%e2YL 1S׌9M>R&#bRӛ#Af69NmS78kӝ,9u3g>=%OS=9PZxg:Mԡ%4gɅ2hF+tP$lFE:Rp9(5CJR#ESʕԦ7善9:?e)/:T<OTnxHͤ@8UVժWjVUvի_kX:Vլg*$ԺVPT:Wծwk^WF` X o+ �6ֱld%;Y򕰁e1ĊqmhE;ZҖִ-?ġʴ5d{Zֶmnz*2}m?[׸Eney[K7%nr;]V׺X%lok)֦󹊍?f{]׼mvo]~Eo|;_ڵf]^·pl;`7~oiX'q ~p5aFm?-@ s'F1h5[XVx xS<cزSuVb?jwf<rd, YDޯd&W5vCP)_8Wsme\Bٻ]żf62'6~-,b9'wtgƹƣh{^FWҹuj -i%ҟh3 iNOҡFu!;MzǧVueWV<uk:ʷUy=lbkg˄]lf7{Ts kgWUve_Mo{mnv6!=oP޵Z7h{[OwoJGx ^<ŃCUwx8q!'K$9e`|81y\s::UV\z<t-ҥn]?}0Qz֓[uCZ{uu`=g.Ua=q{Mnwy?>xQ3E|!UNxO ?yZN|5OyΫzF|潾Η>=boz[?{}j^*E{R<|+!gxG|{]}}^k'Y~_g~t7DSctD  Ը d4CS{t$ !$Ž3mC$%&t‡d)D*+€[.Է/t0y#R33d45drsCI89<:Ck>=t>?DfD4#B$CDTD^cD)sGԵH4IDYDLTM|NDwB<c'E,G =T%[ElEM;EX=UE]^_FF#bZ$5\dT@;FhitjFcWtƖmd3lpG1#2st udLj[ExG&sײ{T|}G Ȁ41_FPȅ1HkHd{HŋŒȎ,Im�!ɒ“Lɹ[IC|IE,BITBɑI|B JJ!ƣ|�THʨ䩪ʝ3JT}ʙʰL4KcJzE\Bˏ˸<˅˽LJŏ(LlK J\F;LĄD.ʴL$.\MLL$L<TMcnjL5T'פM͝$-O΀ ݬDNkLDԇC|I*N:(ڴΔ|,Dρ+O<+,{ΐDM5Ϥ �+#PM*eP||ύ\+% Pи  }H ѲО8FUQ:Qm#ѿbQg QQؚ!mG }K!ER+'&.%E(Rc+}'u"+ҩR0e1!05g+5D|S8]:9NS;/4=Ak@E/?B5TBTSE%F}GTJ.ILT<J%4R=SUTUbUDsWխX# US\K}YUNab3RE։V;f--e5Tj}^V�Im=g;h׻W#rUstUWdWsw=xmyW|mn=~׾qց}?EXRXc؆PUX|�؋u5ع*{EYuِ=}UYtٗݭٰbx;؜U@Yi١5Mؒ5Ԣ7EڭrZnڨ=ګڭM+[1[$۲U;t۷-5Ѳ[\ۼmu\ۆ;ܼM\a@m\]W̵Sݸť[΅S=ɥ\x<%55]h]3}ݔcՍ 5Rޅ9}[#U[9-[߽R9[5 RUڵ,\ޭ^^ ߫ߨ-M:_MMZs*]ߡmg_߲ߜߙ&__XD]uV,e`tOuLޜ``|_ NGXa]TeasM>‹a+b=b vN M]b=`'!+ba&bTl.14i( |Ec`$c]b:;67:~J@~WA!5dBVDc>Q?BE.WF.cG<FddMN^NIWJVcPV](O=dT+SvTn O)$m[^5H]^2f%bdULQijklmnopq&rfSSPufvvwxyz{|}~&F@gp>f*w@cVCCJg*Nj΀$hh~C^!i�G0eF:Efio藆Ϙސ�Y`�p2h6Kcde}(`1Ѓ2pjN@i .fjhjj A-�IkR֐9��B8k k~fA&l6lN~jiȶ.e]캾kilF*3Ҏb>m k jv6ү&nnE`^džlmb҆ifk.Gk1jfVkGk.PfjV-l7EomfoaP߂n>w@Wgw p{kOfsfwn- !'"7&TP%g&w'()*+,-./01'2Uk3W5g6w789:r4;ȇ|>?@A'B;xfތ;ȃFwGHIJKLMNOPQ'R7SGTWI"HtބXYZ[\]^_`a'b7cGdWegfwv]hijklmnopq'r7sGtWugvwwxyz{|}~'7GWgwň'7GWgw'7GWgw'7GWgw'7GWgwLJ��! ��,b���� �! ��,b���� �! ��,b���� �! ��,ma���� HXȰÇ#JHŋ3jlodž>IɑO^ ˗0) %͛8sj͞@УHUi4ӧ;6J*ĥ8ZZM\&Jس@4M0ٺKt޿ E* ^,IŌ#s*rF&![f͆; 葤K=-2j?~6ٴވ;{ <VZμ Fےf͛?7G^:aB{ @`^2v罽˟O?P2g& 6F $@LCp# (D "604$(4&H'> 5PN804 5ib 7M2" "` `$LHux!@��ba id@Κ[&IK�zF 2 pC.kQ7[0@G! r@l_8!N6Ij k#1ƠY22u(Ќ8[d`/&**Ρժzc"B_ 뮁Fh)"/ IYG6_2�#Ϊy .�PS,@f_- +N~c@ ��(s`�\?qc�`@{r~)r~/S̜.I Ȼ 28q 7 T2ڬ �#,8,$-�-Il8,d7h_o@#" b8k 7rEPpV�9an+ PY@CM d�ÀWx d@ K W]`܇ /o=?>ן>?;G=ᯀQ� 4:PA OȠ-,V 4Fh&0��4BG2 gI$ �$B0a#H2#DLWR7$y"V =aZũ3qh槊I09C (\ H+B$XYh@ �ͱ[d%/9(=֏ A(@2 7,-$E�Aoc#gY[>EAn],a(hJRB RcX`T.92oh Ѐ}-B?'uS͌3 F+z@NFT%8Ph WQM mC# q~INe�` (8c)Y~Q6姢[S,4=%H$RATx*4hR#(܆4t87:Rժ@"�7VhCuPQyT0kNS8 H*<E5Suk^*2ѰA? :B}d!Z?fzօd;+͒=-j'> lc@ ΖAq2Zƀ%H[p4TYm?]Pn B!M0Fр5Gd`�BE#i&]Z%ư9aw P%8j� @  ξ}l @]jJNބ]L\*V)#l*TkeK�Ѐrb8 aH"?o,mIF8,Lr+Άmb9j!'CCY� ��20(ͭn#cr>`h` hNcn2qd&~,G.95D%֌a Vp�p ʞ&ma `�"qrCV JEz|vԟIxj�ܢD( 4�E4` PL*n�r&dUK�"XCey&4 n�\ΦfXV�(,ep%1NǦpѷkwQa E'~Z98 l( >�@3 �N(i3*<` F^Rtw> ^F;:}Ttedr+} 1 @'Q X@�k(Eԟ2�d` 0J4љ;y ۶n,NT½;ݮ~-?g2OA 7>Q[?lo>@-_A]>?PQx@?gOMum( Dže*U.,y TN"8�sP1Y {>bծ?쯯%'KD6"0 eB`r~=cGn!CpmF78n7R C@gv6 ۀ�gg7�%7i~P3&2F�m.)5~R~l!k3FKM77Ϧ+ llyy�% fȀ/~So!ca؅ "MD 2Tqq7:g~C9u@hQTq!, 0H ~qWrdys  2tLN'ṿAg*CDŽq(tFS@X�uRGu u'hMXUxY&p{gw�Ƕ97Y�(%xww>B?XsX?긎ӎ(> ؏�9Yy 8n0a=9S:b1"),$Yr('*aҒ.%0G2"B=!y1y Pymg!N{ z3zw$ "{2{!b_K*ۂLYr_6} 68+xbGj&'djFp$0g+<J �_0-)P$A6duPd.6dIĖͲIXU~+}%uw&1f1cDEzQ$LdLXVUQeP;jB3<3$'0NyA N7e/(Oh 4sQtDvQM5.P~4ӝU wSrN5S)7)<KF08c =5S5)?Uxv9x9Z:oq Wg c ^`rUQ$XuX!;C<-Q7u02<3=_XxQ_vHXJ(gH2iHʒJ/NZPET:"VzK2Z*#Yڥ¥`b:[Z$ i<\┓U%\ĕ*k wJH'pʘ$]-7^y * ] ҧP?o%` GG}'`ه�hSvvq)%|TV�X+bcFe)|2+^Y}}>HmS  ÐGY^qbeqxr.._☋0: 駫|g~cv} فrgZ2@Zify21c8spg$ �IQ<  n,IPK89& «6عWP�f 蹱K Jg]5PQGH!s# �y붦7~ٞ:Rk wx>r,^:'w9 Z[X_G9~Vs+ưȴ!u]uUgO蕜j/6U P؊3W-ꗺ4*=xC(ˆ,vꜾ'J[fze:ajY+4ʻӼ΋=3+@,w[U FҨ{׽ :3{ w<P.hxN7; kIϷ9<b)KE�yy9 { wdٰzZ{,—T#| [v4t9 xƯrC,⛚ʆ[I})1l,y5`rbj ˜Qӱ7DQ2 JAp¼+!+ mH)@͉o'߉Q<T )WAhR;i&bj8y`n%8l_{ (  ;VƸ8UNjX9 :#¿PX!F+ˋ)j㊦;L=~-�05`Lz,>P� HGb `7֫z �[p|y"ܬA()y̤ Y|'l,Ml ܬ  +Mѫ:y'p1Y Bz } yhK \!' )}}ї;odzQŚ6F;=7�LX~7-ǘgM)ld�` `/~IX!oJ -G Äl$Z-\R2 Ƙy}g3/s6q<D$ٰ$RF � 69%&aT #Zv_P� s'zfQQ\^(Q{xSK}"~,hS'X%*b@ ݐoA7ۉ9+Z8 ( �i ;j2zr K78IHb+Nkƫ!f|(*�"!%Nc*5N`j9]=ZAW*ENT6AF׭13- D>D#2T)"$ا}OY wYF+x�@VjJ@ ` /^^#'{MT[Ư䐯ʙ! �0}P_>+ zr h 4��ކ/| ^R8IlR Y |ҽ/b6s uxGjc{nEnn/Ρ ׳ڮMعW˵F8nYˇԌ+#RtFqgIQ*%ONj,,.�! ��,c��I��� H*\ȰÇ#JHE2j1¶ 䦮_Ǔ(S\ɲ˗0cly͛8sɳy@ :O@\4ƎӧPJJիXNճׯ`ÊӪpSwn0qZʝKݪ\˷߈ef<y{ǐK;S<bP"S%0LZ'cMmr"]Ps[Lr@ͼyTٷK6wu4.W8q+Ҕ!rCξ{֯f#Bߋ` (2YPl1C%QPHQe0yezug!cSI6 <eh02+@@!8wD9T3(c&  6dt"VE@4<&@�.@)U^p=>&0@txIYS C$e#Aa2Cff AA"j<Yqݩ秠ŧT�ie(<8 ds%W)Zi{ӨPe:J3B�\Yn%`#<(Hf� q �̯춋OPT%H�70H @т;XQc@9 +INkPúT[ X"B@S@ٌ0hA,(_s>m4^B'tG7tlKG4OW}4Rg$Vwm1Z!^)bMf$jm?mx|߀.nr'7G.Wngw砇.褗n騧ꬷ.n/o'7G/Wogw/o觯s/mol߯W `uH�,#HLD3u=8Bn<! �! ��,b���� �! ��,q��0��� 7q*[‡ʀć3xPF<xc%M#Ɩ+tqf̑6I�ųϟGѢA5rfSOEI$ȃ *xׯ`ÊKMv粉k w^}`ŻlB| N@ P[|-4GMEd�ƞ4+pu荪)� gт=el.@6 A�2i@h[QP `cnйv*d7kYl]ǁ-iTf mw =у-mO7HM880.< 5+XN8У;T8! +/p7�! ��,n��C��� H*\ȰÇ#JHŋ3jȱǏ CIɓ(hr [-ct8r*YVD �pụH HyLbW MU2(R٧ r4I4 $ JP "0>%9kjk'XfS)-l2\qtI5w =hصKaİE@%pO�+zz[D�z:G[p ğc"m`P?FN]vUKݺc�'yTq=0h'vw߄qC@%d ҈nOFo7Lh-_Q#@HH rAY#em֙8k 7P�0I4CMrYĒ�hzW)mE%5fe�Jh@W{0hE*蠄j衈&袌6裐F*DUjiP ]馜v駠*ꨤzCꪬj*무jk뮼Zk+6vZlWcVY9v+w��k�,l' 7G,1Tlgw ,$l2,0,4l8sˠ;#DmH'=s:8 TWmuL?dCW-1Y㼵AiCt׼U80fӜDA-wi߼5+u ܓ=42.8ٱD *'n 8�<2ސ ;W`82;B 80F24s{m7P<I�+ez*a;>kO T�<s1 /N2V`;xccp�!1 42@2=`Qqp{c *\y}*SʒydCzh]�, x48hH1 �"ft8Њd [VB+ )Sʒ�r�d�kMy|p�+Q4D4("!4` p<Vs" X>J. zR \14[ )'RvGq�+0C8)0Fw �h V f*Ǎe8.FX2m(c[DA!&OF[X<EL09p ab y�ɼ'!i^;܀DY�T�+� 9q8� p2PYތ1S@X79&ƨ4wOIХ))>`s\xq.e&Ip5 (5ك@_P�Y�@P! @%!2p1M)[ ҺyuEͣ!Z%t P+uh�!Vp'\'KYeSfk�;QP;YҚ4;vA J4iOKv^6f[{.lA z8\mKlkqG8$tjܗ <A�zqC /${=/'*Q NWpͯ~�0~] iPCX@��! ��,n��=��� H*\ȰÇ#JHŋ3jȱǏ CIɓ$ \ B-�@1b$lD— g3#$Ɔh 8Ww~4F@_!j*҂|uU@ $B :@h*D�8p~a8PB]z~l"@jPw7 ,[+WN[+0.Tm`뗉Ҋ@8[ I#0I߈Nmoq9'r=깫 8�  G]} 0PC73vwA58 ZF\P P/d3P 7Fpˁsq䄠h@"� eYc�)ll"\JҐֈ@bh� h` ,kT7g*X[ɑw 0 @%FZ jt~&(3FhM ljAg` 0U'LU&:EQB[o -pB 0@B32` ҚqkC0`(`5\(m|^'P�82aM_1@Ip2!12�)AX%e P[CXSkd9 y)T_n0,4l8笑Jh<{Dm4A$L7PG-TWmXg5t`-dmhlvؠtmx=vz߀6yF�&pG.rD`/ -L6(pgڝOۄ}9ꞧj7~wvm51͎X<IH�_/]Hhu ;zD t ~p` ?|5l52t;e0t [�X@ 1$0:=&/~[w8lZ:p:jf kxC@k" DDkhEaعPLV8y *}M] &aaFx- '~ wW;)0p1]Mj,cI, EAҐ|\}M $tH�.Ìt%ص V8 6l4F(U7ʳ% H29e,b�)p$ xƯ-PD *cɆ1 yxygBSϓ5~n4[ I#/]Eך!䝒yPx,æJXf®Ƀ @ y/S(C!vP@X &Sv (N߶*ޱ �CHNja,zLERyCQjR#XժXqSC Xz¸p\Z'׾ `Kub m+pG4Zͬf7z -c��AˠhWֺc Ѐ 0nwH"1cr.6h@!`e$׹ͮvY\R&CuMJj药|� p zLb[{&�P�P)|ek ԡF#Fa TA1\ Ȁ<flp## XҠD*A ex@c1 T(ߵ.@&Kx+p8rsl8qyb]N3zsbp#�ŇLg/KިW�! ��,n��K��� H*\ȰÇ#JHŋ3jȱǏ CIɓ(ŭ [hdpeʛ8R(=aY64{4S; u& 9336}bT6ō@qP`PI�&On ƁCTia[l )�f$ ZpK[LAӨU A`: d)Y#lM`2T?vsFZAX@s7@uF@ :p+u[ X 7ME/%P^A*(|1DZ0KuM_g"a#p@ v@>HRa؟- �~". B0wDu͖MqR|'Д=Q0tCML38[$ jfYiF@#L2)O}] *'!(6#L&0E6oP^6믳"bz4FWcܚZm-{ Ik ٢ks1�P' 7G,{Pgw\B,$lpB&,#P0, l8\s< 3$@->mLsPGMCRWm5T_0W{ PM=6uAlmr]1x|߀.nϣ7G.Wng掃砇.褗n:䞟ꬷz婿9Ô.#켗{y \.7�<NCNzXG }{x{@H8, C3V?1}<׿5vS%�0)<a)X`㼇9,@`1;ԡ #@e(.!  BqSyC!0} �<@�ẉS\40e?9aqS*>Hp8Њt �N �xq7(NPFcKb9x9@bqoО ɈBqs2yCxɐA`;..$@)Ghbq )P$i9,bqe<20yѸM{y�4iMl2T\,g9Z./+g$, @8T2@)oFgءExS@ M8~|:)J�xb +P cAC C<aNyMIh1*@(7gD;GZsɃ@UBx�VP o6LZ۪5@qH,)Z BQF#7=D4M 9vb'KY1qG9)zu\fE-uUdhY iSK[uNsUpr̶@Aƭ? Ѐ @A'KZͮvz٥6D@cQޖ�h@@{(Io[^ pu|K`W\k`@V�J=ΰ �P L3;C"eMLcWG2 T,zL<r%P jlP.?a T(.8.`(C#p8 en`)5;5y d̢¸s _ c�! ��,n��:��� H*\ȰÇ#JHŋ3jȱǏ CIɓ(9q!܊ M,@+2,vтLs$J N 0j \! Z- VSK+q @a7qPf N.]] Ȃ ?�E-3DJ!čBLW`A@.ՉM vX:YDm*Q @8H [e ~hQBVCaDF@ 0.R@]0~a G`8u~ќwO�^Br-K |aXp ,2P'/%P$cX} 4Fst]hd aR″8  8$oPJIBEsih88 @ w"@"2'Qc@b̆ae ɡ 8b xՉj, p�DT(cKYDcٲ ] [$ 5:@4&6J@E76VDLR@A]VHLP(H`"V#_K@e 8@F@![A+a5%#{p 7G,ogl ,$l(0,4l8<s̠DmH'=sJ7PG3MVN^H 3x-T(A̓o/7240cݳ4 2QWÝzX;G=|77�d;zD ͌Gp` -ב6/'QLeΨXN&#/WЊXеN^̂(UP5馣r+s90F>_8" TL3& ԭdcY{:8ee0�o%福ˌ<cm˜+@1p@0@lhZF?O ) PgY2�o{P; qC̬VLD7=fApba"PtGD#Nr0f&$` :]�%P̀-S$8mk<@eo"7w-2hLƖB TpR9"u-n0P)�q| s!rd#a n[ʰEn`{yk`\.{ 3y�Cv x%,m&˛xYpis /K9q$Y2_i 3pb\t u0A�v\-%aO|L[ih4�+99&ƨ4ǿlWkE8M2P[w "*q c Ip;2 *jُq4P" i=Q5#Ͳ6m,!21{{FRs#/@E%3k` ll(0ҵvX סBbMZiƎYas LPG6Zl%o?COMtܘ+xG6z7Յp� Tz{P|+4P+( �LNp_h�"1c(ΰ7{ GLbC�h@@{(c%gLX'08`:U7L"$̀c#;Ps\�T+�"\L){`?���(X>0p?ʜ:4h(L at9΀tV V F;;L I4,NHL@cW~"D8�aո5 jHEuM\[I2Xр>D h[{�! ��,n��9��� H*\ȰÇ#JHŋ3jȱǏ CIɓ(hTqBnE8IVʛmq 3M4q.)E)òl4m"DӅ�4@Y ՊMreӳ[;PBVd`% Z; \SaA �dnm$?BA %Y fKJPXM"WQ4V4�Ѱ4 {#b:-'0J*.dhfp [2e@PC:f 7 w+vAv+ehXf Ĝsm'#Io!B/l  (P'yAdy'5%N2ee&f@& udS,BA Pv(Ї"IL *M&!89\HB�{4� PMލ2PPBrK mf%!i'xBт4h)xI/�<k8v`U`88@cI�[NuD+3S`,q֊),fJPи*r"2rEL@ A1 Ԥ-QG-)N e3$À TuUNJSpؒFoPL+ر4 t8Wݭ 7P��<GaV�q1e0+0ԺE͸/'K4 ۑ2Vb/8zEbW`AU2`oED6AĔ(AYҾ5JHà'F#mgpxuu|ݒ~Mx7G.cgl砇.褗n騧<.n;(.o|7߾|n'g}D((Ϸ0Io>4R0Ϳc>دD�!X=D`2hLʈu�4 ^7 T^yT<d@fx-$y`˜+yAX nC.! u& &A4)4< e,1Yg}�s�7Du@ кv`]aD�.kD։`hdΡyDEsaq(4h`2�8ˆh u`H^.2hL F. V0! 5iZ0q (; <rp+jyy҈$[!P1 ,sr&)MvtqBIh(cl b߰uk%P.D=~ 跨198�0FaDTBwr"(3U8A�X̣~!pȘ:Be(4PZs 8!*1È Mf:uH (  &´ʅ} `TX$(PVݱ'ӹX}*5C^p]2k [C((Bf5Ί OWefY lsK5VwU0jcōn$5]zyxK� yKͯ~WK� "� y 'L [ΰT`V.0A (NW|@AH8αL\\�T+�"!HN2l\b trxFL*[Y.@@m4b@0|2KVa d@8J64JF?8Pc 2<9a 3J[-X!m0.~sRܠ.pPOMZki=t 71 P! jxC^q@��! ��,n�2��� H*\ȰÇ#JHŋ3jȱǏ CIɓ(S2H˗� @@C%(Md gO ICGp9w4%-+- QG rB3IuI3BJPX >EGa@Q+MG|**zȉ{#b:`Qے/-@.[RX)!rA]V P@ $`8n +d+J`2ZAl,uQ &ȕoe@2I2/d3Pl@K Ԁ] 4|.�@w߉1&N3 (#}ՇP@ nTyNuD+`H8x(`H(ާhRS�'v1Н8߅c d DzAL|A%rS8e2!nAsV;nD31 ~ Ae p� qvީؔ�8zP lZ Yq [R&,)A ) F@�&$I.vҎ( C@ޖ,'rJ3pC,B[�,l' -G,Wlg0Cw , l(2M0 C2lӌ<<s>-t̐ I# ѐ *C?9CPwm:D|_a#;Hstםp N4Ԅc߀ �!2}b@P̃ߋW�,cгL6.44kأ 袷3S u> ;@20P+� Ȱ�l]3<ǰgPGl("Lۧ1cO3٠́;HCI2(JLl*A3~H_#`? ` T7�S(;LЊ&"� V�00!:- ��! ��,n2� ��� H!)Ç#JHŋ!#cƏ CIR(K\ɲˆ(cI͇1eɳ'ɜ:} J&ДE*yҧP]6uՏկ;;KY{۷pʝKݻv˷߿}SM<+^̸ǐ#7Orc"Q3lϠCw,`+;\װn-Ñ^ �Gk\{qг%')0En+Р~bRKN9 e `A cQSw=/ʖ%Q_�4kأb^2438 #@Se!IȊP0At"ԡ<ߐ}6�G&1.˜0^z0L6H@)x Zr<V:&4�<8d# U82i(�*ǜ@Jx �e B #-WCzzqЊ^P�a€fz7)^L_mΡy-#έz1 @ T @ bO3W))W^d ^ �ɫ:d_6 ic{ C@9ue'{=L<@<�Sǂ^W�dPGG+pB/|)e2%$zٌX<JC�;ị{C1"G2c&$0]AlQ@5dcLS C@%z8svk-K <Ptq,7�N#'Z^Z$ T"@\1!88s* ~cnyt+Trm�!eB~ՠ .PCϓôWg:81W/>{Ǝ7Ƭ+=Êohɣ;#.4�㎁(<�*2'H Z̠7z -�! ��,;���� GB 'oC겉xѠw6Z`Żl8^T�<jT",Рu\Az͒(Ak LԱGM,3hAȞ4`P E+ �dӀ2Z +v*d7`%m pp�my[Ag,bApSwΑca f,<qcE<u8U3w Wγ��! ��,b���� �! ��,m����� H*\ȰÇ#JЛŋ3jȱǏ CIɓ(S\ɲ˗0cʜI͛8sɳϟ@ JѣH*]ʴӧPuJիXjʵׯ`ÊKٳhӪ]˶۷pʝKݻx˷/ҩ(*xaO&≯ǐ.99˘3k̹ϠCMӨS^ͺװc˞M#sͻ NȓУK9سkN:Ë7}跗OϾ{˟{=Ͽ(q�h*9IHYi(b|h"u%s)"r-(p1ho5ިcn9c>d2IFbJd"> RNaV^en^~`b6X&d)`jF)tix|矀*(f~Z>&ʨ|>7FJ){^ڡErbjJ*w]bj( J+t\*k0JlwBhl-rώ-N[V-nn-ZF[2n. /Zޚ//Z0 sڰ_1KZ3#Yr'+sr/[37Xs;+9@-Dg9��$,wL34|5-uSY#4] G&a75e/g4Q p-qebvixwb xu7x θnu8o:y}_^yv9^y擃~ܒꬻz{㾸޻oӭ7(GW[=۳7wZ~rWzS_?Q�8krD`@�! ��,b���� �! ��,b���� �! ��,��?�� @*\ȰÇ#JHŋ3jȱǏ CIɈR\ɲ˗0cʜI3eA t ϟ@ JѣH*]ʴӧPJJիXjʵ+xŠK6fӖEYnURZ wÈ+^̸ǐ#KL˘3k̹Ϡ<:riȧ[b&[T ,`Tc Nȓ+_μУKNسkޛs7[keZL' p׻Ͽ�(h&`FrFlF| :#L"h(b%"/c&a{5ߋȏ$#!7|(2?PF)TViXf\RE.āǓFL]NyF"H"gy`e Q #iB)'}*蠄j(BBi\e\B $V2Ǐ|pjF |$=B r 7rj뭯ܪ뮼+k*bP?q1g ?$ Rk Vb"l ?�C=b.lF{NFT:`!Vl+J&+$l(*l?fHȮ|+/.j3l:1?DlkB%wD}+RqdG\KJ`-dmhl-6tDBDgԀE"Cq�"wwt Ifġw;~r]5x!|FБHHN~,E7`|D� G/ԃMug?q__59]sFR_4h/JA� 2!L: b T~x? Tяia]`�ɠ-\")P[.! *x vF@k�~*.3(Q?`?dAG I~ԣqp3kpH:ڱ `@Eạ?D@$"#1? .AN!$CWLNB P򕰌,gIZ̥.wK]2 GhA>`7.3G2 ,$ .bf6�LaZL/]"(5" ,D=.d T6`�p D) tg/7юz -[Jph"^IR^A(cRXv! [GʖjTЂP!Tǻ0(!ժZXͪVծz` X* 1``|b T/&u*q# 鏽>+n@Z2hxk\bR8XbUWCq85؈D pJԪ g%ktBHнp:["T-O@T!4qHh<tvRAͯ~�LΧ?ShSзEBm)l<?(/@ U" G@a` ۇ`SV T'e=?@-J8[zEb kH]E.{`yىk,`;p~~ی^mv/˻RRDq~^M NH . Ѓ0wN7N{ӠGMRԨN( z0/u?F>WFÏu5e;յDY XP6 ,(Sf[ض>CmI0|fV~�; 3Ot5uN'#;A Dx9q"(aӚf$ w89 N@AuA8Ϲw@ЇN,&6a{ߢD<]QO7_&It 5 C0~,sx0xY?rOx|*pDyxQg +c>9<ۺ 㒫onr'~@f~pW@i~OO;Ї~ R�?E!R@s'쯟};tO?`շCH׀1}$=EAݧ~'}:O)g} "8$|G|ۧG|'{(|-8{07 ,A0ۧ: z~Fdp>`Pr@diJJxXZ\؅^`b8dXf=hx:`=mg8hr:wy|h4:04tx8IȅhXh3P3WhGH404@H@�! ؊H=@SpXx؋8XxȘʸ،h(Hh8zyH8(CP蘎긎؎8Xx؏�Hhxߘ%9Yyّ "9$Y&y(*,ْ.029Ⱃ<ٓ>@B9DYFyHJLٔNPR9TYVyXZ\ٕ=`89Qfyhjlٖnpr9tYvyxz|ٗ~9Yh b ٘9Yy9Yy7ٙٚ9險9PٛɚYyșʩI9ɘ͙ՙڹٝy9Y 晞깞ٞs9ɝYy�:9zuY ڠʠ:Z)ډ "Z&zI(,ڢ.2:} 4z8li:ڣ>z<B:'DzHFLڤ NRP:VzYX\Zڥ`bZfZdzjlpjntZ9vzx~٧:jZ;霊ꗇZZ꧗ZکʦZZʥ:ڪʤ:Z:ګʢZZʡǚZڬzZ ךܚڭ*亞ZJ皮ڝڮZ Z:ʙگk:;{ k; [{ ۱y)& {*+).[$/;ٲ4{ti8o<j@;e)DKF{@J˳L۴8PKR;/[VX'\+^bd[{hj ۶n{p;tۯv{z[|۷ۮ;Z[Ẹۭ[[z۬ڹ[:۫{[ۺ۪;Z[۩[[zʻڼk1۷[zثӻzڽ{p+۾b˾{;k`J|\ NT;||Zܵ` <"<f[&(l,.r2\4\x{8:~>@Ą;D|F|ĊJܸLĐP<R<Ŗ[VXŜ\^Ţb\d\ƨ{hjƮnpǴ;t|v|Ǻzܻ|ǀ<<[Ȇ̻Ȍ�KĔ찖|Qɘܨ z<|ʪTɰ\\̬|˺,[2 f,̣L̞ʼlܿ|�lڜl<r\\x~\τ||ϊ�А<=ɢ|Р: ==8*ѽY"!]Ҳy( * k. Ӄ4-$}"j:=<@B=FHԗL]NZR Q]ՐyXͧT ^`օ9d=f}ցjl0 p}o=zYvx׆|{ׂ˄]ؾ,׈}ч؎ݖ̖ٚؔ}Ҝٚ]͊٠=Zڦکڦ ۣ-ۙ.Kە3-`۾ üM3 ܨM-ے?.]JpƭDJ /ݚ@�"0୰ �+ k �@0=6< ���`9��c@I%<Ll��k`c[ߏjN`u`>. -@(~;�+�@��eA.DP @` � }` onM @ P ܠ0  m\?f 0�Ơ -kN  ` ͯP`0 B0NNŎ^.Ϟn>ٮ~N^.n>~N_ / o?O!#%'_)/+-/1o3?579;=O?ACEGOIKMOQ?SUWY[O]_ac/egikom?oqsuwOy{}Ɓ_ƃ/ƅŇʼnŋoō?ŏőēĕėOęěÝßá_ã/å§©«o­?¯±O_/Ͽɟo?߾ӯOݿߏ_翼鏼_/ϻo?ߺ�O`G@B >QD-^ĘQF=~RH%MDRJ 4L5męSN=}*eETRM>shLQ^ŚUV]N-Xe͞E*XjݾW\VW^}+`… bƍ?dʕ-_(fΝ=Yhҥ6Zj6Q[싯iƝ{mݽ}p 7\ș?sխ_w=h[ݽWIx!7^wݿg|ǟ}? N;#0A9 pAԫA'P- +0C.԰CCG)DOD&SdEVt1F9qFopGj1H42IT t2J2KԲ2L43MtNM7'DM9sN;N=sO?qOAc tPCU+PEGKtQGdG'QJ/}RL7ELSN?SPGKTROTTWuKUV_5UXgJVZo*Rq3[{v_%6aEceegivk֣7\q%\sE7]ue]w߅7^y祷^{7_}_\ `F8afa8bӅ|8c7c?9d(#eWfe_9fgfo9gwg:h&hF:i~y" +j:kk;l&lF;mfm߆;n暀��! ��, ��&�� H*\ȰÇ#JHŋ3jȱǏ CIɓ(S\ɲ˗0cʜI͛8sɳϟ@ JѣH*]ʴӧPJJիXjʵׯ`ÊKٳhӪ]˶۷pʝKݻx˷߿ LÈ+^̸ǐ#KL˘3k̹ϠCMӨS^ͺװc˞M۸sͻ Nȓ+_μУKNسkνËOӫ_Ͼ˟OϿ�(h& 6F(Vhfv ($h(,0(4h8<G@)DiH&L6PF)KDXf\Z`)dYfb~p)BIxf|u蠄Fg&hv.h>*餔hiv神zjj*)Џj$ڊkj땩*iA^UkZ mK+AZ;z-k6mT2n鮶JdB.J@${˦. K;H 0۬Wo,$:+pǬjܰWl7C<1?1Dϫ.{׼,'uE Xs|wL4"r'-5|B@4,8-Rրx#bW vk}vr/wJ4֌K8*;&.z4nPMqi}{Wyė欓9J+{.xCB;eWMv7xs2S^o59#;K:h^7>;BG>fKƳ/bs_w]<1dT_D0Pa&LᤆBS0 cV%NK7ġؔU iȩ2Oe%:qMC|⣚(*IV," +"H/Rb<!5J*n(GEu"#1} F!!"DF:"|$6Jϒ$&M"$$(C)QnS)ʏ2 yudZ²鱥FtyˌR:Ld/]L"rd*$3͖8s r Ml,7Nj3t9щq:ĝ$yx'yt~"pJNҟ4+zzӡ(Ƣ(F0On<*RX7$).Ln䤷IK}PvTM7)P#ԡ槨FRZ2u>N}j|*P*VףխU^ XJֲ8*ZNhn}+\ 5BsD `KX!b &Oc#R62YXG,h*B*jSkղ6}-Wc+\Ҷ,n[¶-pk7=.xtܜa.t#F7e%vrnu+^}.zs2}N{K6ط/#+Fl38~p",¿0{  C X&>qի Ŵ1c#6n sŹx/,f1W%gN )S3Vrgp˙2/#1Wf 51n~sc,йΉ3=~ A/>t_0yy4"IץҖ 3Ms-t[B-교i95ϢUծ cY+uWrY5"aWƞ e3)~vS-PI6m q'>w!,ap#M l#1EV2M�v4CLp#H@ˈc(=lft%arנ<*_6\Nq\C渍orn<3adVh(L at:1ed#i^ֆB@2>@2<+p8GͣcnP5 B9or|s#�7sCnG<S-ѳ5S/ճ֮=c/]Ӿ=?6?>i3~>-s6/s>/us??g۾=�|8W}Է x}}Xw8~Wy~ 7$X&x*,؂028X6x8 <؀>B8DXxHJ؄Nv78TVx=Z\؅C`hb8IXfȄhOl(Fu?H<h963ȇ0-*('H$h!Ȉ(H h ȉ�'GgNJ'Ggه֧Nj'Ggnj猵'GgǍ獝&FfᆎަƎ&FfɆƦƏ揽&FfƐ搥&FfƑ摍&Ff~{ƒxur&oFlfifcƓ`]Z&rXFyցHYQPNP6T)>v릕ƕ^`b9dYfyhjlٖnpr9tYvyxz|ٗ~9Yy٘��! ��,��K�� A*\ȰÇ#JHŋ3jȱǏ CIɓ(S\9˗0cʜI͛8sɳϟ@ JѣH*]ʴiMH3]:JիXjʵׯ`aB)5ٳhӪ]˶۷T, ݻx\t LÈ}8ǐ#Kx1Ɣ3k̹礖WbLӨ+G͹M"�;۸s2&�P؇k?lh&^ w`Ҝ^9=[N`4-ƁtK$@<E ߃F(MهK(SG4 KVh."0[mM0Wog# `@"r�BMK!bL6i3Ty%y� �r5My7�@ "uO暎E҈+e-�PR�8,栄Z,yCrr  f"f]!V(cK;w`pğmD3vr뮼$gzP�VT%k@M d@ i+vk-ZgT�+ڮK˓ +p'\o 70?,MFL?hqw\],$l(,0,4l8<@-"{lH'L7PG-TWmXg\w`-dmhlp-tmx|߀.n'7G.Wngw砇.褗n騧ꬷ.n/o'7G/Wogw/o觯/o� �HL:'H Z̠7z GH(L W0 gH8̡w@ H"HL&:PH*ZX̢.z` H2hL6pH:x̣> IBL"F:򑐌$'IJZ̤&7Nz (GIRL*WV򕰌,gIZ̥.w^ 0IbL2f:Ќ4IjZ̦6nz 8IrL:v<Iz̧>~� @JЂMBІ:D'JъZͨF7юz HGJҒ(MJWҖ0LgJӚ8ͩNwӞ@ PJԢHMRԦ:PTJժZXͪVծz` XJֲhMZֶp\J׺xͫ^׾ `KMb:d'KZͬf7z hGKҚMjWֺlgKͭnw pKMr:ЍtKZͮvz xKz|Kͯ~�LN`Τ'L [ΰ7%yЇ>(NWɃ@B,8αwX1hHNd92L*[Xβ(y`L2'R6WFL:xְ>π4}fAЈN4 ]dE;ѐ-J[Ҙ03N{4GMRwZԦNW-hTհ]-ZL5w^X׾iC9͍&lЎml/yζ\mdofMrzN?uݑn7M@˻7~{^eFO;'N[ϸ7{ GN ?6Q0gN89ď}|C@ЇNHOz(!PԧN[XϺַ{`NhOpܭ"@�QλOO;񐏼'O[ϼO��! ��,�E�}�� H*\ȰÇ#JHŋ3jȱǏ Iɓ(S\ɲ˗$AʜI͛8sɳD@ JϣH*]ʴӧJJjLXjʵמVÊٳhӪ]˔۷aʝKݻ+߿ ɷᲃ+^̘ǐ 4L寑3̹75~ Ө^-6װ;M5۸M413ȑ_n4=22R^2߹ 㹟_>wG/ן1߀r& Z6F(Vhfv ($h(,0(4h8<@)DiH&L6PF)TViXf\v`)dihlp)tix|矀*蠄j衈&袌6裐F*餔Vj饘f馜v駠*ꨤjꩨꪬ꫰*무j뭸뮼+k&6F+Vkfv+k覫+k�,l' 7G,-hkD^w1B,L(g?&ǭ246笳k8Ϝ D@3l PtTW VgOXo4u dg$vhy nMdۍ[T K N@9+/;$KN[kzN*2촇.{ҵΞ'JH+|t?/}eOob_}`o]/\o>ȟ~d/SoR߯?Oh�ҿ#<1:'| Z(  zeeZ& SP.|c(CҰ!گ>!Q}F<D1{"')ZV-r1y^b(;#s5Үn|㸺9tv񱏛# /'AN|"E2p|")ɿQ{$&Mέ|(CQm<eB_{%,YO.sZr~ӂ)#&2e&_3iRqмMq&8q欜8ө<tc|g)Oų'>;G}NL@j;浍'BPm?Pj<1Q |?ư{f3$a @>:O.pi {Д6E&N!+HOj!t �0L|h}cPv(Vp4]*>=#QD�pBiX A<����d� ��P=3Aa ��l2AԟG6[P  @2 Zu��@�Pm8�ږ% <Y* X`M]:(w1$;*;a:6<N{5jל3`170+LG>1{IP-bĄ<1+.n$c x6%sLǜ1?)!F.%%3y$~yM^)VL(sy[.,^z̜25n69v83w,AІ2ME3Ȏ~#-&SP60MӞ C}QZ>u@jʰՍy5gYsLֶ sͽZ󚟩v-l5 eRϱ4hS?ӾvZmY۩6w'q ӭu+~S-лK7W}[GZ|'?xNp�8ăϤ 3.s7FB.ry!9-S.c~|m|ls 9A6/F?wѓN3~Nc.uWz֟uw=_7z؇>vgyov=owyW>w׽wy?w=x'>xWxx7>w=y|Wޗw߽yvw>ݟ7w=zpۧv꯽zj>گwv={d׾طv{^>׿ug=|XWuO|R7?t7=}LWחt}Fw?7t=~@UOف��! ��,b���� �! ��,�@��� H*\ȰÇ#JHŋ3jȱǏ BIɓ(S\ɲ˗#CʜI͛8sɳ'MH>2'ѣH*]ʴDN}JիXj ׯ`ÊKٳhӚ8u۷pʝ{+݌v˷߽yB ,È$ Ɛ#K\d˔3k̙)ȟ;MtЍQ^ͺk~Mmų#hTs!/Mq>AA+@pɏki&Dvj4i^^{t/�YA +lIDC(D ��T  P7UE ƖENA 40i4 [d`/&@@4�4cz`'4 +̢XauPFɡ�7rt[PS$0d 9ac2(4 ։R瞰I7ԑAH@ � �(0 (� =K ��@vn8Q|jjkNe)0(h� h` @р 29%mǥJ V*PIh&S�0H1�J$̶nmRc%RGHK�0H9P-pR m@DzƎWYA! T"tӞB6*P%a)�AJ`"/)笳l8K@s-H et\K'55PWmFTo\wYd$Ug6iWp;=m u|7Lu-P#^8i'ݐ%WnXgw砇.褗n騧ꬷ.n{ #M kgW>Z)󻓶\sEfgLJu}}DTDЗ tB@GYx㕷DAhAoHB?,1AW@4'Jo&S\P_}z78@-@&T }@P1 @~C�S�RTư[ XB<xt`(CZCS<uaBp@@%%!C$@DHq]LjxCT@8P3%!lX;z*Bؙ5 "2 HGJ pd 2PE0ͨF'ӔE61Ih%$]:$%! pDoyW+/EdN (sf>3xMfsք%>hJD�n @2m!I@(hP/ Ϙ-. p70 5 D$~u jZN@ PCd9(z&|NJ:. lKm +(H12Ք]rC@ P0~N}c#/�0TjZ9$HD (Q b6Z"[\ZuyɪV NYĈhJ:wVkTB`ap G@Dj;#  <JUά kA` xYf/�r+\>+ h ,awn Yl{+>6lteKսnvu6?i!EO8Qr)Cy+EUvNEqD\I8r.i a*` bNa -4NƶSdjbٷe/| 2lc(ap8EيR+;~$&1I,CZG&@� X�YD6.�f@/yn_Ι\%+0JB369s~:xY cdlclH+'f3#LXGJv $ɔLːC +` %fJiiC P�p"$&[Sknc׽vq(f =.A}mSW܍1A a-8[(` η~�NO;aEWAq1�p@Yf,|!-#2st!7ULK!g0�Y c hCts"M?RT!9_HA蓫kSGHԃ �p!=aL"40n,v;F1Fː�ȣ]Mqrl4!BDhD@q<w>%�:�;v"$4/lWzSl=n$~w6UՑ {��@FOgQ;w<)�Bm?gp�_huOsÜД<:L^޴L�` VLDG�WLyG} �p #O�Ib MMA&M4h~"ȁ:Ёă~G�w">}v@%W 'z@7v'��P2Pҡt�MIPIiQbHBu^rEPtS+5PE "@R S'1eHu1HFRTb+��2 (6Rpi(+P(PVS z)ЊhˆN8P5@8Y[؅񅆂(9t� N(YWWRD nTZ�Ws9W_.وX:4eJaZo)*".܀U "8$XȏX1 )]HbOH?rŋ(.XX=~E~d[�pd]/Y\HuP� tA?qJF-e[5SօQMDu%+< $00G9\)H%0)U݅Hhipij3y)R9}:'}lwu(+pvivk"s5*b5F�F�fEX�+ibI�0%f;ҙ)*,F"�-Cz"(g"&*U5 i0VzI2NI�T)Q3`ѹ#֛yYH�)Y/29s)°�@0 GtI)21 P�VWmxcfp&ifc֓gi#&��gR g㚧`I.zh wDzg.V�! )X�-Zh&*fٟfYe0 ivƣgAg*EɟTg� *J/N#ڡ*QB1r%O'qB*&lR4 ֖G֔eii�*e P(nZnVqOuVP l!n(CX {iz 7k1gJƩ1l?fY#S2㖨ڨInd<ʨgm?V^@ vǚHI˪:`EʺnKqٚTъު �V Zzگ�kS{sWȂa8B ;[{۱DH&{(*kX3S+;4[)۲#6۳>̀Ió@{HkBKcIkQ&P7"봸kI G;+�>{+zKC]z'\{;P+ P 3 T2Ptp/05�r8@PrO[2  @�&� �0 2�ua&[ ;2�k!뺞  F  � n@� 0;1wX2ET̶uMkc�e [0 D�dK s � �d @�z� c�P `+ � {븐 M<\{P� (@}⽴� u0 ͠ 0 K͐ P I06 \�L � 5|9, O{ʋPƐ!lba­5 iP ` X{ (U^\aLd|0 "[Zպyd; Z^  ,uc)ޠƳ a �@ p �k�@@ ͠ʬL0 c@00ɕlȬ<LՏЕ,kLʪckI` ; �Ps0˨[BLĀ\[ f茱l (ϛldl4% g,i\ ΨcP� Ͱ ` c0 ɠ� � � ˾<00#]Ҩ0 �m6& -I�` ?CfE� X @ ( e0 lkk ז4< �p̹ 6}@,&˰m= ajp P a�},+Z @�RY�8֥#U[H! [-ڣCڦ=a ɫ &Qm,۾�ڼ :}Xɽ:Խ}3ѝ:]]9⭴Z }: FT=]}}C!�>^~ >^~ P&~(*,.02>4^6~8:<>@B>D^F~ $~LMlM..A�OR> ]^. DqQ )h'!0F�7Нs�ZN`pxnP#.EkTb��i {Ai`qu^zNX2[ @`襎~~Aq^!a>}2s}�XBU Ehz� w �k뿮nV@@��Nq>nTQ>)k%-`2-1>. @ip%i`͞ N^@t~e®@`7^ 8DI"m!^^@RƧ" P`:# +B) GK �_v 0U/+Ѩn0 ` :"ToOo+9  A ҠL2quy/r~\Jl/ n_"Coh 7  ^Ob9KF@PaeE�F� h0 "P }ǟ/ZO/fpjN7P�- (`f/  ^�a@ :h57 \VAv� @z�p$@jFRg#a&@leHǓ=}TPEETRM$L?Qp'RN LephR(I�Km7B%i6YZ=[iȰݼ�آrdɡ�5X)I�nn݂kr,i@6�*6j tŁˍ|rгivL@ T&e<Sݽ^xPR5(j֭@~%dH  8�w'*:!@'I  4p&c+(0+H3  Ѡ$ bP(d D@㰢Xp '+Z <)J+ҧЫ* :$ ঀ1Z�j 49>$}MiBVgGsF)5꧀mF B tS,H(bRFI X~sCOR9;̔н.Vb=$XcE<H2K2Ϥ\g G(�a@ Ӟi@43A w܁(`}8�t݁a@p!l(p7T\r wM5l FPM JdMnHb-~ x%�&`m5V2wWzM"%Dei~zYHg=& E�h �2 d;4mQ@QѶߎ inP {젤�9@A$xѠ?ʧ˰eB+  'p&ۧk<p^Wv^jG>y篎Vy3{_Yg+h {ǣgizw|}?z_�bi(�!@6Ё _O=Ђ`5A)E$<_AЄ'D=8b'ZKa e8C0K+D@%(@@u;ЈGDpXF/F226"bQD.vыY"AX.n -0EӲH3~эoap{@`dIP &\0aI>  J* !Z"'c*UA9`;$D q?) )QAPƂG:(KZ- u�af4Qsd. `Ҝ|`+_YGJ'HOd |0 2S'Nm!T'qFBd|$hFN:az=UNg1IQ#�4`I gpB�E ը\Q;J/J@Ǡ`tmVG .TiGˆ:uTxI , 'Y*ŪW$ؑa iËZL6Unldڬ*Չ;Za�D YP/.jU ߸bmn{;Wy�R@BaLxe Z/ ,9v׻,;zbinzՋQ.Me4AjPo~:P@@_%`rmp%<<طp3laSJG^p 8SP=oSab ǐe1e�`FM)ךJ$ D.08 dyO,)d{h_y<Un+Dk:汏d#Ő8G+Zq|AQiI̝=+'C3̽,Ii3faB3z<~ X70BIS$\a`I +�V b߂$[ F<˸}'5j7<\` XưF>O>=# JF" @@Uc'�ӆKx3lԩ.]:  05Q�v  �C9Rnըa6({bml@v'jR�6'qc;?F[rLJtG�ر< lCM66;� ХrD@4 tXaҬ5P/р:Cw-đɘT^Qc|ф@4Jvc޸!}mϢ^@O !;  C3g=7)q*}p_`Ow �=NG�G\wH_KU9t <9g94n6DR^4K�N� PDP-e:ȗ!@ٿ@( +�$+X �PZ) 0qZ:PXR , @'@,"yA2>>4j`�ػ) Jp*,1 e؋hi3^~#�J!Y 8 4$?4@pCB� Z 鋿 p2DFT Cpœ<+=BCtD@lMAUCS?VB)$d%K3 y1@z`9Ƥ Hu@:5NhC(%!KK,%I<0 p<Aw+(;a_ɑYF: PT$Ş~GڊHX,lFb(\\ ]$�R#�_PY,؝[EЃDF'#9Y)9ѕɁITdV�84ǓhB)O9~t-Fȥd(JȟHJA Дd=+8I* !(0fhSu8ᙫH-4XfX FX-r- )*4r)ܶd̄ռG MwȇwЀt$-d- dlȓ̟͜I9-DMޔ-䔁@LdK�2h(y :�FಛHPG*9~X�#q_s|9މK PDa�QlGٛ9<  bz @$X킮Eȓ0QNP۩QѳtR]碭O`ܬ9R2ESOK ji85S " <S? @%BeA5DUT SW*3"1K T,I4TԦЃE`OFx?B'".L^C % $# [-ZFJ]EN \UU U<գՠպj�Yb%zh�5tF]793UhYMpe@sK-jՂ֩:V0W ,hW@BjUVs5X hUǀ$_5�z7y55\ӵ3,dݶnp3suӸv8x;V}%$"t-ؓPXuXZXXZSW#5h̓;Y86GقHkӸ0q=ȥ]ꥤ: hp+M׹i[^:)<<ۓ:L]%0=#S;ʋG$s[ד[;_Z[ZKػ۽5~۷] z(�M#3ڽ3Э;\{nr[:4)�Q.~U" wMk- g@y^ ܋-A??F^WHAHX4!4 dߡ( 4)N�5|A %__ݝݫeE n��A� %4Bxy�XK/M- m .L>ڂ(�͒C ivׇAJ0C (YCC8 GTLD5$*(�@*lD$Da �!.;1b6CTXDp.f%XP+İj)xal}TT*9nfpa7ԷHN߾5Ρxhj`^bvH-ǡGbLȆ & K:+O&�PeGP�Vw[F?~|c6[ Zd&l1K >GvFbh塀IIƵ=xe*޵jK}Nʝ`B2 'cg}h gg"F h4.JKnt6X+u-N:)zgMLZ|N{ӬውfL̒&.i ٌѩ,i@6 jjQjt$,Ђ96iTnMzDfέlO뜭۵iAP"ɮ?@~gJPQѫ=PmUN]Ҳg( !.랐m=mA^hm& n+N#U6san�Oܪl:+s~ eV SO G_P`"�o+pp ?Eݞ WwqbaWEӴ*V ,գ(UAsO#oB$Ӹ}Ȁ_~ t5e¡Hrsr( .2? 4׉4ޢrɢ|�6`mk9c`/0`(t7B/ D_ >A{pY(kZzn"a7+Xۑ[9L4Ue [e܌�Qui5}7W8uۤ\Q8X8w ZOuUv%8V}ScՊſ%R#V_wGv44T fW{KvPvdi7[womoscm)w@cu}tG[m%�XE@X䭽νE>߭ c޵ Y.&C{?G/:}fzޑxk^_ 惽`xC./%U=k'YS=qZ^yf"]yy; z0z(ymy+Ekw+D}X�MKז-AxBܧ:D4'+|{_GJ#c@ 2hőWo(A Aߵ|Q| |'G?dd}:7B( da> Sb=Fc*)}}H?32bB>)BW~9$DodD;>{�(Hr,3@- $8@$o4*�c\䖤�� X0t)gFh@�IOB�`++NDC z)G L5 lӓZ8uDj.޼z"vp'RNM3`ץTm]f3M-h>e9 u'Ln] Ҧ :;RC=m7W_'}t/&?…k-o8kDEQr'] ŝjw xYGD块^AW�LF}d�E\q "-aHfbx)K@+P9PЈu=5bEh9W,]sa7PntdOYny99Te%i&bALr9 WdZNģwYڏ WP?ls, G^ XKAivJagtu"(I VYmNR%S"b=xXb5cAh 4J ÀdCH B�74y'#VN6Tb=?sB:ѷ˯� z{[PJԏ+pAK '찴Z{WD&=�%3EJx#ÕNnK?g<1绯]-lt^V̮A<\9|A]tAen 7\7߀ &`46HO 0Ps t{47(@pP jH pHA@u3(��pV_NrAϞﴗ&馣nꬻNh9N]#_zApPcpt;Mgݠ $!OwEtJ<$u 7$P|+ҷ>4q&6x=΀[RBb% FcҰ6!!將>F�xF<bZ.7B"()(:Zǚ;1-QJȲjDn|c MQ#�h.v h4<$"E2R,!cHFR$&3AMY/T]Q<%*HDrTRNL@"X C_1+TI*[JgYA-K!v w_Py/,9U͋'I;ξųE\=g0,:$AYĦUO$tE-0ҹNt>| 󩷊EhOD(EAqi(K2 "$�+%jRp4` 0u9mcX A� l)^*.B%Q1:~֯C5P*�jyζZ-P@v~tk _uSh�EAՙ/~e�)}�n+ M}jTؔ�u ,+Q�vxPͺf.q`h�HB? : TZõv,Tڦ�i)+Qx@XְّdcVJ�Bh  Kʀmb7e!26 (l!li&2 lCMW!DaD0L`h\͆Gూ8l14hoscd P:"X<1t�>Wc nU1$y<Yi�ʫzrp"�X7n qH m@8a4"`.+;+l>d&` 88 4yI,QJ} )M�_Tv Vs^B= E&G'?le Thy``׵qMUHM�=�5lz ynFq" |ޡGɋu_[Ֆ054e6283l(ý{i/d3B6qJ3TN =iZhbp$(QɬAܴe'<KTn^$)Du&z > +p՛Lfd@8Xs-1SJ#=_$d! W؅5F$BTAi3'>'NM]:fE>M/L/d!NK8.fvd; ]h_w`+3OAQa�=מC\6tD>�ЗDre s�r]_pP{Թ ~$GExp` ɝTOTZ]=\K](IǵKDʊ ]!ѐDAAz Ƴ�4 ꊚŧM !@pQݯ(aOp !ѠKKR LD,J j<I-aA%1(`[^IfJN, 5`z}D] x04}=J1\[(M^xM 8ɰ̴̮ `et|MAp"&&M*8!Lj �'ڢ&M̬"ƴ %%p�q$%!'I%YΜ",~#1J. 7�l75l 5֎� " "'uZ"DT]L0�Oέ]H^OJE-ApDENI@I|P=JRϫAOCE97(QCb!$MF@ G_JF8ʵ5c gt$D6V"ET2Mp?Λ?zڱ%`R> .MI B_f`^H15P14&fR2p@�HT%^|ffc( (fi&lQhŦ&&oof&Jpq'rpF q&s>'tF!"t^'vf'n.KHv~'xHqExx'zg%Dz'|gp$$Un'~~{{'#u u(&E.(F6(<^('Zhvf6燎(FgR牮(o((((?ݨn"iH2)^R"&% -iӒR_@)rj鏶<DTH:EފSޅĚnP:^-[ioΓ�,C}^H)E}TМ$"p[-U%M xWO^Pw9dRq*ϝKܗ@�mWyu!tt fŖ[GvUMjSڅiIvVt6!WDeND2d)MjAt+ե څrkVi>^noἫ4+jc!pqUAeaJ^*C pF|ї}Jeå^8IaAxlMf_ A=|WRZᗑXG1ɂ,YFġ,4>΁LB| Jj^T,F_h]z{-]YYm-XMZ♞< mSY,eB�axq]l<)0A�'dr1 EIܭa[dp\Mv\A4.jZę <j^TDBemAM쨕ZF Į^D[AZND8onŕT`\A$09Mi+lC@յ\ q8=DDF20)K,@KlS TU]ZDEp/D8.^(JP%h.Rp 4<]Ma٥&g^w^Q^ PX .}}'c|iCdȶ)._uq%8@6TjnXq�" rJӰm �De\HC ܀: MU+9�fW0�f _Ec$M=:, ~!R# -1(@H1x. 1\rF^Dp RJrpať-"j 63._0 G2&:V;G:#ʀ!T/ K 4|=,e #B$΍]c�ct;K,Dt$ܣ]1b,r G?sc 4#h cT%"E,@3,0M#HOtP/,04"<#LNn2J(^ n9Ċ #;: #<D?3z2a $MBG"dMLR]<DY]+]8%[G*MrK׎dHuO>5!),�?b?Ϥ$R,6^dKrX$H6NgGUUA2sfd4 eh jwݴ5uǧ_Rv vc6Rm^.49_H&ef&we>tz iVRx7w3'jknRmZR}۷_\7G?O E8_8+'~}fX88ㅈ88u888߸8︥nx79븐y'"792Gx_9B9o9j9~z9ux7ai5jOR. ; НMQҞEChzC%mʽzӠKH?A4"HZDz_Tj_\:"};h>Tk^Q듪CmT]k,T62xzʂ؃=8,K5^*,NĮŷu kv]w)WI{V+R ^j׮{e+Ng9Ut+!lU^b O~{+(;XzA+s9<R7l+VW]c׼{ʳSG$ɨ?" Pe7ڣ,tli{DE\=ζaUyAn,^dֳDڞ ،~=aJۊ�sa%"=0Ø}$6=КrcYD֞XlOx-=FmYD}3ڇ~H=ֽZ;s3,YsDq^9o R ?^e *<Rn^FLJ.Kؾd^+@en2t?JG�WMᡖK,/@(� rP c"bE@ŊM<+H<7\VŊ(U,8 1j@(A$@ҤD.AuiSOFҿU  ?q*)lGC[䖤��jJ(ܺw#"5&4+X,kF@:P#.ޙ+u ^(L5Vg _LQw�n-cpbD#Q YQxW08f͢]R+`Qq۷@ZJ#r,#�qZȷk`.l:pk"VϞ 80AʣA4pQLQ+Š*)h0l,p�Vd 2cd�*$̢y`[pGKqI$h BM5hI3ͥ^r̴hL(g7�ӥ.lJ=GP`)9-<IO c# *+ԥ4#*mʟ ܃?c* H~Y5K8yHmʞҹuLnLUV9+o_؂SdoMť*"�\#aӥFPDI7~ ؆9$5v"�p{�zGWޥ܌]x `-&A @+z QURE(DH䓱Ӯ -XX]JX# =.O{Wu)Y@݂*Xd="%m[6mV;] rp|q]*wsk ǂ@i4ؚ�JP .H�'Bn@-XMz :u4<rd�s(ѹ)]6s,>-r\rhjV_R�od(UO>⥦^x3"o_ G^O|5%4 xi@7"e8<c�gPr[SҰf,ck D92_Bp-|n<o|IĨ"Sh e`��4zE\EQC $ $a:�} e<cȁ"P@갏TYtJǡ- �"hF4"aTw:dlVbpi>vn( E)"ܸN?iHD�x#V@,qa /)2QҔbyȈ8�ZіOV^f;S�ӏCWɔS,X@`YHeda=MI*Zl; RKA"S<%D.-fNE1ѩ�B@%5i@=KaSt)x!ISocPNT*}bS8T.M �8)&`(UNq"Ve5YuՂ|5C[WΕbU+R֕}_X5H:R)`lV[QK<!jN 0n})a V v)KLN<"uIdXŲ)V0/x!o\vGeJRIZĶIQF"el!K.ע/(p^+ m[ "B<!Bh� /} 7 O0i] ZD\{ ưz!^藿Monc1yN@,\ c1L }KN}~`>@dߎU.qͅN A!܈�/@77gnG+2,pI#b1/YAPo6bf4I'PxHk(E,ab65UXBBVրFla,5lV d&�[h$ lDiO9".?ya FA:6p?̍nuvv-mL;ɽLHwo=nEYS6".Ƚ#|7"\rõ] r-ai;\DAv4TX <˚�㐹N*߷^9_N+wA>_kяƭ`W;Thmk?}G� pnujE[f?|b> S|>&>iŰkLL{nGW Dg6WP}c)^iׯ\'nڹ۝*޵^{y  j[ ʎH;0dw+p_[vG GbzQ+rd p#R|ۿ ~#"*tXEK J. t+ܮAXa ",* .ϼak4Ld0kP70: ABro\a5P ,*b�n2~ x0j-�.H0,ň,M `-"`F *, .&� /onmV,Q 1`P+ HLmkv uq( ˥ŻbLE*bqBQD3Ⱁ1OV,A`nu 1% \q-q +B r!! !#B!r")"1 " B"-r#9## @#=r$I$*"$Y%I$E%er& &͑&q2'&J'}'͐'r((2)()R)r* %OD*r+**+22,ɲ,j,2-ҡR2.rr...r/rҫk%r0 D03131/1!.32)S-'213,/339S+73A)?34I(G4Q'O35Ys&W5a$_36i#g6q"o37ys!w73838s3993:S䦳:ʮ3;3;3<SƳ<ѳ3=ٳ+rS s>Cn=>>s?=*>'s@9K? @? +@ASAA)4 B1.!-3C=.CE6T%sDQtB4EYtKEetVtFm0_&oGOF}4Hͫ8DH0GIIIGtJmJTF4KYtKEKE4LCɴL1LєBtMM@4NtNt?N3Ot>OO=uPɳP <5QsQ;Q3R%:)RR195uSS=8A5TysTI7MTi3UU6YUYUa5euVIVm4q5W9sWy3}W)3X2XX1uY Y1.JCUKHE[%s[c\U\%\HMt\LuG']]Gu^F^G[,_5^^A`2a3 6" aa%V*)bEa32b1>/c* adId#^AaaeYed5^S=lfqf Vfg6=!AbAgehuhvOWp~Vg!g6<h[devcvjj֫6kCckYVlcle*m]ٖP[*ie6P]vd6nvoo]VmVpfs6gWo@^fr)r-r17s5ws9s=sA7tEwtItMtQ7uUwuYu]ua7vewv)7oUp7wuwwyw}w7xwxxx7ywyyy7zwzzz7{wWGd{7|w|ɷ||7}w}ٷ}}7~w~~~7w|8X 8x!8%x)-185x9=A8ExIM!UxY]a8eximq8uxy}8x88!18x8x88xɸ8ո8x8ٸ8xy 8y!9)-1'99=A79IMIyQyY]YUeyi8m9uyyyq9y؉9m=)y7y9yɹ9yٹ9y鹞9 ��! ��,�E��� *\ȰÇ#JHŋ3jȱǏ C8ɓ(S\ɲ˗0c p͛8sɳϟ@ JѣH*]ʴӧPJ@իXjJsׯ`ÊKٳhӪ]6mʍI�x˷߿ Llݻ+^̸{C0˘3k̙Δ-MӍ?V,zװcˎzvֶs͛rrqN񳿏&9س=Ëg}<ӫ_<{˟O6-ϟ{'_& tF\=(F!}fᇠm8_ h"d"Չ,YΌ4h8<@)DiH&L6Px#UiXf\v`Zdihlp)tix|矀*蠄j衈&袌6裐F*餔Vj饘f馜v駠*ꨤjꩨꪬ꫰*무j뭸뮼+k&6F+Vkfv+k覫+k�,l' 7G,Wlgw ,$l(,0,4l8<@-DmH'L7PG-TWmXg\w`-dmhlp-tmx|߀.n'7G.Wngw砇.褗n騧ꬷ.n/o'7G/Wogw/o觯/o� �HL:'H Z̠7z GH(L W0 gH8̡w@ H"HL&:PH*ZX̢.z` H2hL6pH:x̣> IBL"F:򑐌$'IJZ̤&7Nz (GIRL*WV򕰌,gIZ̥.w^88L+Ǥb2L)63τb48M'Vdb6M%v3Db88N#3L:v<Iz̧>)g@c�MAxP&5 _Ce[&zaC&hK0J�fEU< R& JR�X0l jD->j Yx.xp\Ԣ%Iя @3lp"3ҁ<Ȇ. Sʔ+.1Ylc�Izi⩀LWӘt.%@OK:h8�|%@6a "1`WX62p F+5.Nՠ[@B+fܐcڶf6+hel+1 p8 �7p[U5pgVabt}1 Jiu]V"Խ/Dk毳/Kޏ� ׏10;'L [v70 { GL(NW0gL؊ Ʊcp `JV@%GK%?2^@V*`mXFVS" 2&A%f1O1@�0 "`0N I Jl5Ln Kg Z&kZAeT�|3@�Y} 9P�@ZX P@ !Qh� Bl�.�P=U͋@4$ Ck†#@Jp@X7 i3׺deSLz z>] :�Axww][x8p[\@ W-tcp(`U7D`@EMr[ZPF" i $`s\ kSM�g+@တ[ `!�n<;'2 V(cЃ˜v B>({ge}(UM�;ޣ+ 'ѴL ` a@(0MЏ~*40 ` @}I.IB 4eH8 ^ [7(@gQ�S��-(g=APP{IP �+ƀT^n=&]P�te{� XlF ` F@{Ft|� � gf w|l&@v�wd�@�� ٧3HHFmivQdFXw0c te0 @P��Pf y|� 8nx|%hx}͆Tp ؆g LJy[ PѠB|O7LJSivDF�hhP�0 $htXwW(YH�Ǘ UT7S+ l"PP��'zgzG} (HtI ff؂'H�-`f�xtЌ%A}WEghx20P؎'QPw~1 E79F &@ u Vr' rP ` i`  �p)ck@ix0 P�cPLj*ɒI ĥ�}6 "Ie@P_8Q#Y]iUP `}_^)Q2�(`P0� 0gX lkzj`` 2�  ǘ @~8x2@f* q�@nP`g{�@ vZY[�]ɛ� xV`;@@z! �f@ �3y@E E O깞ٞ9Yyٟ���! ��,b���� �! ��,b���� �! ��,b���� �! ��,b���� �! ��,b���� �! ��,�H���� � H�! ��,�K�� ��7� ` ԡƍ2�@@y� SG2�z)cH&KDrɀ��! ��,@�b�#��� H@q*gÇ#JCH3jѠE;IrŅ !!,ɲē CsI3c8s Ӡ̚@!J詝&83SJR>+կVyb-ȵӯT&ͺ@SANUUpkʕJ_LE A{ KY>AA+ۀ>6[W҄VNm6\͑;7c =@I8PBJPX!:@7�  `EAV+oC+a7"f)!$VfiCN;TdF( x 40i4MbU]ywB�7"8 `J(iE 0 9@ n +ud@2�`䑄K ـ@ "/�Pv*8JȘm;>GeP-U\uD+ @i8aYEpJbvhI&B+XAMj � Gwjg^m$(5&P/u`bp-i&!4` 8zYd̑ T @R&,B9p]vzlYyk]: Թ ,YHj:.O<N wiP\Snqq*$�! ��,&�H����� H ��<!Æ B\>THBܠ0P # 8cW@6-p [F�p@?cP�!*d@� *`Q=i '*&`+`)PL �A+$Nwo߉1�! ��,b���� �! ��,5�H� ���y�#@ A��4Pƒ >$PÊ)F$ 2iMA \à(VH}0_B Uh�@Y@pHÉx-XK? l7 Va8Fg�! ��,b���� �! ��,?�H� ���j�#@ ��,Pƒ >l(q!Ň`)�4䄀2�B_4u| o2JDQ2AȠĈZz5 �! ��,b���� �! ��,H�H����� HP+Ȱ��! ��,I�K����:� J p$`L9(cbd (o@P',<3Р@q �! ��,b���� �! ��,P�H�����H * �! ��, ���� H ᄰR#C BċXF0I54ɲ˗0cʜI͂DXJ(Y.D81ect \LxDUZ|&Ʀ 뵬ٳhӪ)cY6S$;rZ-L$Uɼ鎔t.9IN+P0Lnj#KL2|H+qX`Mci@j7P@"+ nh �$%Q⏌*?w0``խLU sQf횳G74C�8+@X񦲯c?I"rd DeVhM,AЌ8e!( }" 4P8Qd9Y(lF(!D#Ȣ1<tRb/va b!i Fd04PrWf_F&7x扠ynWe`|a]袌̱LH>#=�S%,"9T9"\@TG�'7c@2q&ZЊ-Hpi ?Uzil!F JA%:(Jv.ʱmZU. 6C,i(45pDoEV"P+jB�@`c d�hD �KC\$@PJ>,e4{+l%;HCgF-5Z(>NnuC,"AjT+ \HLfL =1U,*grS0TI֝Dt?>ԠJ�@OT!5BBg +^0y]tzYG 'biTi8<m~co9DߴA-G$VȰBHP�98OkT<AɃz�G6D�-OCHV `4H^i<)ZW- _B9K#p&+CBjB{ص*-s&�PJ- ` T@ z&PL8/�!Nq<:eNNc 1!%vq5nXbc!~i!v IP~aϔ8«NRRj0)gBJtr,)$ cЃ0,Jq 0  @XZI^*Ќ4IjZ̦6nz 6YfL'IBN_&-sꌧ<wJf朧> {&IQO'<ІZHg1QD VPB-z?4?\W(#gҋ^ 0iZͱR.'}F T2 Py.֤a?IOn2CjT_)BA˪@}7U` k2YBSH:SpkYɒ)^׷WKFMb:d'KZl6zQu#dDk҆֯$,JoJQLkGm(MZی Zy9W } %R.=s /r:J{4PJM@XGZԥI;NVhW[jV !Lun^H(QJWn ptۅݯ-J7L}*ɻ- grF'l^ n^/z_u&V W*5ZgaɅx5S([;\+w-8 ]S_k)SY}6%2 _Ո/ ӖdW]kR j; sCVOf K95+y]>%X`g 8f&>#xsw5vx|'~*$win EGnxՇsu5-4x~[&Ct-yMQ4ntA^...wG@n[ri烺egip{oK־7iuY!a1‘6fWZ̽1gNn8Ϲw@,*ؘ<{y+J[O^jWi3zw+]f]{TCpgƹ.+IG5 ξ]c�>oQd'sǸVb=xDon]N+i3>!o<ZcxMc{K>g8\w@[zs\/{Kr>e8QK'Om rH/5km{}'(o ۇhg h˗>xz<f~pikwX7w^wt|V$zgPWe[k7gqf [qf}1XSi6(wtg/V6~Wc&b~Fv+Ewf|Nhs׀%f'he}TWmMI{'XdGngeSR6nc8xW[Led%dWr|4n$xaMqeiutj8|xp&&pexmXjŇ$7vOuxl'tngVg19{fus&H]heguX8XxȘs(HgW&uΨp`[75,KuSWwm#eD87ua/&`x츊jᔎ rD&_n#ha^f^F{^4o+Vz2Vhsb7`oGtc .kh{mǐs菡)Fnؒcy(ǒ39Wׇؐ2ɑ>l} fgpgl`j8{lf<(gQٶ#Xuhf)j,l`lDsywdgU귖M胛opX[aHX[y{e7Pg8IufYt&Q0шwX`}QiH$eviwYisFiWgi9&娀iz3owt^2n}FhZp9v'ih9F֩dn{!ɚh d)Ɇ8by}fv6Wgꔑ(4y>qɉ(膔]b94 [E'75w$M~E*U9UJڈKZYOPSQzXZ\ڥ0GNؤPjOY$+Ww'p 0hxToJuamD*`lY:fWUj㘧CZVry ڣ W;ojexڗh:ՒiZlZxê"iym:79*I)Cj y6JaȬZ9|5zlDCu ~yj;eD2g')9dxGdJTV9&}Y>]钩HfxI�$i9:ꯧgD { 9B)X*l9 pym9w7ښ+{j6>9xj +DZJeϩh#)Shi (֛_ǩ_6\{9˓~X5ۓJH )cL6f\^ۖz=xꓓVّ:idZP $zpZz}ˡo Jph*N[t 2&nVG$j)8J 奻Z|ZJ|[jL[] 11۽;[{蛾껾۾;[{ۿ�<\| (x�D{yNjv v9wDux"uiϛ[�('uK M^fj)IJyKx?IYw:y]ɡQ3Z"7fTvy*ġEy؊ļi˭~1a?) =ɐkvw&go||}FTk#J #r\~Kg攱͹;e̷YWۗĦl~zꢛ{S˹ cD4\Uo n;\)_iFIʪp\aּܲhY TlOܵ}Yz+>KMjUZh,6kj7,YXmcGyʲLllŎ"I@ #b ƺxzcZo(*̹7M(%U=ClR̟||+ݷ YY\yi<9w,]ˣC0*!L*Σ·d֌i ׆k÷z|~׀؂=؄]؆}؈؊،؎ْؐ=ٔ]ٖ}ٜ٘ٚٞ+o-7HܳyKl9kѺx֤]l+t5]`]"(v jG=k1.KǸŞkg+-w_QAE}Ϊ},{߅g,bJfϝmhl{fFJIkt7x`\GݛݟК.*ɯZ`Չ1|T"ԗ5֙(n5x,̬5FyɈHi?n|f˃q<Wj4M2}4F.>XoGǼiOk<%5> mk ةW,+ě1ȗ Z:Ql k}گ$~<Ͱi. UʳL~\k'4>mD]u^ݹ>Ӏ|w, ".ʅo^u()WÛBzԷܬ9@- ΧINëžb6B|nw nk܎V \!cyN=ш ۩]c.L >lt"xQ~qظ};O3&JK+KלmҟI&:>lx ^t 荗>Ƽ.;Pɴ{&x|H> 1"Xe\8^LrIr ){ăϚ~wɒ/Ʈ{͙y@/<ڞ_!.o NԚgBWqo` Ǖ.J&e߱OɥϿe_՟ڿ?_?_�A@@ DPB >QD-^ĘQF=~RH%MDRJ-]SL5męSN=}TPEETRM>UTU^ŚUV]~VXe͞EVZmݾW\uśW^}X`… FXbƍ?Ydʕ-_ƜYfΝ=ZhҥMFZj֭][lڵmƝ[n޽}\pōG\r͝?]tխ_Ǟ]vݽ^x͟G^zݿ_|ǟ_~0@$@D0AdA0B 'B /0C 7C?1DG$DOD1EWdE_1FgFo1GwG2H!$H#D2I%dI'2J)J+2K-K/3L1$L3D3M5dM7߄3N9礳N;3O=O?4PA%PCE4QEeQG4RI'RK/4SM7SO?5TQG%TSOE5UUWeUW_5VYgV[o5W]V q'a hX]6"gmX6%jՠ>XXc$ev *i%r(\=hl m]פv`9j 7!e]!.HaF1%_n꠆�~-c=H\ fEpp@)` o:Pp� P +Ȁb=X aQSa�-f H"b&k�r6;衋曇fa@alHB@䈏V+WY\-6c8[O|CY#mef@rf�py#QAEj@d4�i^ _|A.v ("[~1k}n+F{q'q7u)Էi�c6pĉ ǰGkn\0an.W$"n ~(:7 Tm F19Mc؅2$ 5W,[�`zBd(5$�+쥲 h@2X +XAf-+Aa*k@02qNfX/Еe#�V0l " �Pr X" Vm. ?xci̡/� b5�Fc5I=hoA6Bh[Ԩ|Bh0&p0kch@V0j"$H^ P\a!B,#oX`%6ĎuR@ezI@h Tc0\рk 92c�1I$@5)f2^ IpX>"2I([N cAO/Q2-GKP-`m(@È�I$z/Hv�^(*ќ'O*TcA&Q"H8UwcdIJ%F+Hc| ffJcZe*�*0삖E܆PhU0;�X0Vm�pc$@@ K`20 qtW*) ܀`ϽK`n<l�! ��,��|g�� H*\ȰaCH##ŋ3jȱǏ CIIR\ɲ˗0cʜI&DrɳOO JѣH*]aͧPJϫX{mʵׯ`Ê[ٳhZVpʝK݈iݻrmۿ;LÈ3]x_'L\Hs>V2ӨS^3+Tm1@*PI/(L?M^μ-M`5if Hc([\zGSʒY-BXJ@ޤB*@dJK@lRz3YD�.@H"&Dz;)�LN܀�03ͧL6yK0v+!,Am,AN N6oD02\B Fܰ8BXHC�'#ɉ)b'8jhK7Ҋ(T"H䩨dK0 ` ƨJ`cnc*!b'A ,#ά*: 0P*Xy@+Kd̠HB�,ߚ)&춋KܰRG$mA@ /Ѳ"*E�J # y/A)Z(X1cNЪ ʤk0,]1AnRuD+ ؙ8wDp6C2R8QufZ-=*Vcmc;܌Ɯ,{3-EɴqҷtC: B4Ê S6'$)-hy-4ENfI{MꬷJԘ@.a8e82a Hf+n*ݙ;Ǐ=#+p �hI4CzAC,�OzhQT@&X8BHSV #b1d�pp(B2�5j @FXL=iD2vԣ5c2$ HL"<&=<aD%Zs=CB@Z,0ۢſ^NH:6ixKXE;ɣ ͲG> LiȨҐS$'FZ&)N咠 DARL%*=V,gIZ̥.w^ 0IbL2f:Ќf-UIjZ̦6nz 8IrL:vG(IOFB̧~S/l ςT48B*ʄ2 (c*O$�xFfQ5J(HБDia��gRy&5�dM(CQ��@rSb2iJ�2\4yQ)K#Mj�AJ4+UZR:�hF�Puo+jP.OY ӪQՙIL`MЧ ,�CW#;Y%!%(��faR�@��u�"%V,(8P6d1d#%@Դ8Alp,! xlk\Э@\UJ%ѶWC4�C0 �(ผPK["�@W�P$lLl2�Q f F U p=K�2/pSbX;.SV@_x:@KXV i핀 g( �@, �~T f<fhm2P�^1FkkQ5�@,!�,Z"�usO5Ji[-LoZm~s΀B8 �FѶ/eԷbbSIW*[k[gހ20&<lzCzEkSi6TJc+h@m 9f*rm| Λ}k�QG �tcTX Ғc[4و7 ~S] �5HX�4?s 4n67h8,�� H)a�;jsP6Y-E=Cv.N򕋣Kmg]] b.TIiop@Jʭ^K8~nZ{!<UBƈG6ַ юGhmo iF}%ӀYC�p.Fw�~ȡ9MxÛ].p}ʃpYRڗ�X_ҶḐ$ҭ[JiHF Z:t�AD`H-0ӏɰxhZ_Zwq�}LaI�~A~|z||}rXڵ@Pf|6fF4p0/�&rː�  xP�ŵ-P'uP� Vc*N8҂ 1 BB$赃/@ȁe3qrE7R5qu�U|E~(tw�6~؈!ev7|4 TU8Oi H8|؊X! x؋8XxH�ȸ،XxؘHȍ荧 8Xx蘎긎؎8Xx؏�9y ِ9 iyّ ɏ � � 90!,ْ.09� Ґ >@B9,IZIp@۠DR9TYVَF)6ibp &۰�+ Xg0۰��lj�}�z`r9YIY \ɕK) � Ұ"@.s;i% 8 i 9` u�ʐpYy9ɘ99"0hހu P`ހ a Y͹ V�P Yy05٘ Gq}ހp& 9m)! cPi` �U:Zi = ء  @' �ZPI ( I4Z6zh P�P?oc Cj+*ٜ8R:T 깣<  e�` P^ b*P0]a:M�3ZvzxY %V ~5%ڤYZZzʩک*:zڪ*:ZJ*@�$ʺ:J::IJMպ܊HתJٚ:zEߚJ*庮:J*:3zJZr+ZJ�;Qt ; +*{[۱$[K;&,{(-2/k3{8+Ċ"> Q* D B[HKGLK۴P۲OTKS[XW\[۵`_dKc[h[glk۶p۬; =vsu{|+Ϛ}[ޚ Df丐;I${ct2㹠;"{bJẰ;!{a;w!{raȻb];J!{Eaػ&4[+J滾ы {K ۿܽ˱| , |;| |"<[&(˻,|.24\û{8|:ó>@ī;D|F|ģJLěP|R<œ[VXŋ\\^Łb̷d\v{h jlnpKs|`PW\#AJ}Lŀ` �l,"0|C4�+0}<}{��ɭiLʚl���˺ܶ H0�|c˘aJL�+k`P\|, @`u`\,\ʲZ"9`|h�W@ |<m̥-,P @` � P0 ,ͳ @ P ܠǯ>� `lP,NQ` b� ` ]^ ` lM��` *-=0}؈ͼ{]3{٘,ٞmV֤ͻ}ڪڴڰ =^ڶͺۼ-ۤ­]f͹ǽέv< }ܚ]<mmmM͸]߭tm4H~K l ε >uTF+V"^^IK(?*{.n0>4:6~s:7<;@B>]F.HLKRN^9XT^$`df~}jc汛p^o>Qv^rϽ|.lVނ~^爎 n1锞~>鿚>>vQNId~^J{G~ Nt~j.J>lnT/^ 8�>>~n . ?Z*o"o_e(o~.$_3/579;o=??ACEGOIKMOQ_S/UWY[od1F[ a/eo iD_m_ npo,nzKzDo|??uXt?p?_?_ȯ�! ��,b���� �! ��,b���� �! ��,b���� �! ��,P�H����� (Pܿr3FC�! ��,.�#���珀&Gܻ #&G@7y%J(y5N$`;p E*yCXA(ѓ"4J>4�b=oFg�u9j "ZzuT�T""= @ul: q3 1n^ #`/n_mԠ7uIa:xOgO$Ɣat-1_:i l-_'Wb>pl!N.Z7o?Տ< QËD^D׏oo>"|'^#J}ه~ %gxA�! ��,r)$����珀*\0s 2H=ɓXc‹"qѣNj VGҤɋ �7$8UXA(ѳPgN,a =p)=0)@-M9qV%�@)&؃J԰ AD B\&| #Y�Tk\ WlXfӀ7S㶯ˍh`gByXKltߋ FA%nM\l;3w =kïO4fl=uS:9EF;zj~%7ᖣ}̳O[nWKEMQ B :[T6^bQX@��! ��,P�H�1���� @*\ȰÇ#JHŋ3j,(pǏ CIIO\ɲ˗*SœI͛&eɳO: JƠF*])ӧP,0PիXI:ʵW[5,ٳhVM˶Qn} wݻ#޾7+a+^̸ǐ#KL˘3k̹ϠCMӨS^ͺװc˞M۸sͻ Nȓ+_μУKNسkνËOӫ_Ͼ˟OϿ�(h& 6F(Vhfv ($h(,0(4h8<@)DiH&L6PF)TViXf\v`)dihlp)tix|矀*蠄j衈:O>Ce>RC:C)7lꤥ 8rJ�1.ii T�<k@A86+ d�xSJ&a+�f B 4,@�"擭rK:?FA )�c 7 SA$=ˁ;lCIpN?LZ0:=@*?3=@1hd>H?<?@_?S8BXB͟?P>̳ϻV5tZod^7 vKMvf}djvC wrc@M@Ѐ.4r3Yxق x/>9�! ��,��R�-���珀*\ȰÇ8 ċ3jTh:o,nIdGYˆxn˛8 vT�<,s ر@ P恣tS,a =pjJ��@-ֳJD{Xug-5;W *pX|rT @u ufJ0cUU;{2KvC;rp'l2w-ekޱ36Ϟ:H) \htV{G<[6b>plӟ͛/1S� x *8 :JZjzx!Hb"*'�hb8h:.#A&4E I(AL�! ��,��0�&���珀*\Ȑa>lHE{y7ѢǏ0pm^G(bd8)c&Ĩy%OD@ P恣sG�D08Fy�AkyԤ8ciѮ AD BZe� V�AT6ٹ!a>u�NBLf>ij`gBy8y&mԠ7uJ/~lа==ӟ1c쩃2;)W1_:i&l-2/o|`lTvo޼}=?ϛ|ǟ~ oXZ*`s-`W!`T* e!s-؟n8!Z"nax*rb�! ��,��0����珀*\Ȑa>lHE{y7ѢǏ0pm^G(bd8)c&Ĩy%OD@ P恣sG�D08F "R 2XcԝURDu=-1XdkfтPׯ)ݾUH� V�A6ۊz"$ @u3bd3i@)]qWY*3 �^ 󒵶gۋ pp'lg2n6Ԭg:?cSɁ1eSTN _:i&l-2G{b>pl.>]1U�>̳aG^T)x 9X w! i U!ua@��! ��,M��X����珀*\ȰÇ#p]3jܸu]HIE6oɗ0zd1sDI@ XI *@ТP!z 1U*�d�ƞ]*Jui}+[D dmݒ{ȶ� V�A$7ݿ v S2IɓP!nt3mgC %m덱e#mT⦮Kذn=xt z4fl=uS&5;z#E3-�A3<<B x!LjI!އ}މ$t)f≃(",\h5c4�! ��,9��#����ƍ*\x0s1H=ɓXc‹"qѣNj VGI �7$8UXA(ѳPgN,a =p"4zaR 2Xc[ԛrJ�R LԱUjذbu*L f-ujܹe`P #/Bu)COp㎵8C}5 @;{2KԪ^N6J *qSl&g6>՘yAr`LF陫F̙/4k6Vˢ1aN?ӟ3<.[9 �! ��,4�H����� H\Ȑ`†*HQÊMF /dHLtrŖ.Ed9Ӡ̚n9gL+}JѣH*]ʴӧPJJիXjʵׯ`ÊKٳhӪ]˶۷pʝKݻx˷߿N7e[v1vOe|`fG@ +tGH�D08{9F 5wn LԱ;8E3hA(r 0aDYo &̧P3i@)]>EPA$Î='sƁ;lCIpN?N�3 mL>p3@1SR哎4=cj<2O82`cE3<"GIRT题c&D�! ��,,������ƍ"/۲ #"O]eev˚A䘐"I8�"O3n-]rXA(r?� "hCO)@ 5=-�H)p0Q8{+�D dڵlz[A+ 2/ZnRPVm>u%@: q3 1n$ݫ %3mh^9RJ{;`kY>ע5fl=uS&0|flm-筶_kf 4o޼}%$#(~j4A�! ��,$������#"Ѳ #"\ei& E4+QcGpldIoipTHQ =r&T2O)XAzʤ|�@L*҉�,D{Q+f-3敪A{`P w󶥠;L�^4 ͔.`ƸLv*d =ŽmT⦮xY{&mϟ1c쩃2y̗N<-k6g0aD/7l<ۗ}ʏvRA�! ��,�E�~H�� H*\ȰÇ#JHŋ3jȱǏ Iɓ(S\ɲ˔^ʜI͛8sɳϟ@ JTh̢H<ӧPJJժLjeuׯ`ÊKlήfM˶۷p]+)ݺx˷ʻ~LÈNls1ǐ#K6x2W˘3k̶2祟CMh閞O^Zsjѯ[˞MoϷk{lͿ{ Nnœ+_9f̣KNdԳk=rï]OϾz˟7>bo�z^W 6F(V葁fv ($h(,0(4h8<@)DiH&L6PF)TViXf\v`)dihlp)tix|矀*蠄j衈&袌6裐F*餔Vj饘f馜v駠*ꨤjꩨꪬ꫰*무j뭸뮼+k&6F+Vkfv+k覫+k�,l 7G,Wlgw ,$l(,;<R0,4l8<r@-DmH'm_>l'W,Xg\w3"_H^l`c 5_||߀_?,a-CKг6v,|EWN; _!M\.9 /8- ?Ru.<xK݄b-؝K?vX}>#Oݽ޻+w? :?/:T,M sP_- _@= ;\'H n-nK& ?a$,?\Wc}! Ԡw~b$,H"4%-vE-: 'q,nwz+ maKH:csԏE6vC}iȆa3$"VЀ"7Nnl#1 xhaTD#* ;ءc+O`.dYуphEyL{+pG.7ذl -遌7j|AƆ}3M`)68gc2IzPMObL6НҞMBL>:wЅZ5bCD@cE3JҒ{�aIgJӚ-X�8:c6 Pj43"hQP*PL�� �V�E0\¼êP�QVChX$גT�C S>:W,v$U�;Q #%@> |Xa 8!f; J4PG?k_�xI*΁& pl%ŭH8A$"q-le;p$I0;1 pcxF>X[Y@= @ \d� &i-a T*"NYcFm{E "@0~͋^@ HC21 : �c(C5ҫ\(�ԑ"+�k,g _A,FA#�N @!yȝED�,c$#vp+#Ҙ~Afb&oV4 %CP>W$-? 2D1Ա PB$&;ۀeL}�#@%Lb8�@p rN�0l!X5'q`�� NJ<<o�Q&9vg|RC;�=�Ql @ ~ Vca]}�t&=$ &5g d@$A(F" �IpWX$ oL"vEb�O$@*0"}˸cX,n\~8`12ks<Iѡ\$f94q#ٍ$,$;Pk�-ف^c C%zL*_Y$#'�Cۘ� I""y/T�]5? (W/ d'Aqk(o6[Y X� /a �Cd *m|X"ICP1 =.uaj} Ftw3濘Ds+7`EV&-`\]jW pwX. 㶀 &8dR(&`HV&H( ,QvZ2B\]&w20p9-ˡ'!]W'\p[#!0'PR``H`v{S85:[<h P {p"^\VH�acxR"eTfe%!uG$@ �`q � UPW|IdMq[�� (}`8]� q( e Nc<6cye`( ٰ e!PFctgwMG�8,#Ii6(LGp@X' �N"ьXhF�i @ +P�Ǎ(h討ñÐ 4� z0(gw?�AVY` ܰ   BiɸtH�HyuOrqֆm#" ["! V+Pl"&l @ �w 8#;ٓ�pi �Fl%w0_@\1F���IY R!fq8P\$"I�c9$ �3&Y(Y(pn7 #Fd�͠ Y)r\#ї#AhHʰb' "�w_kٖIupPRˈtf$ �n& �G�IsB' P�ꨛQ ib~oǃy'ot0���bt(VOb8ws�3gWP c�3ɛG[ YiHY؜(fYci~VX9 `{RH�G�ڏѠ1+Npi[ *$*G�e�P /:1Q7ȹƀmG#{Wx gQE֠{xzkפOO"/('b:e0}Pu"A@z&@}l2v` xJ}zڧAz( �uDvw|0�By ZS~s g bgf2z%ڪU:%:QR$M$IZ'q4B6P % @ 0 I0 L) ` j: * %ܰ 'q Ҡ� ` p� W$" + p *0 X0%*($/l 0 P � @ r P �pq P 0I[` 0&d9: P'%`۠ )� ({"r  [ %0  @� D&  %p 8 pwL1 $K� jkKry&!�)B "q ,'" �[d0 eT;𺙈# (g˔0c+&� oK  'tlʵ ࠲0K�P:(K{K P` Q2+ o&wP#%" 6 (G |K`(;"+{0 k "P y4\i`KJV0U|$L+O ^@yNg 08L� )�r 0Ʀ+k ƀn=\n,+ȊsZIȃL{X%p; p`q `%B ,bp೔` � ʒe 0 &[AĬ\ "1 ] z +\,;" 3` "m/ +ѠY# 92@ fΆ%ں#qjх̭*% `DF;"*ڮ+Ҳ% ӻ;%Q K"ʀ ;#;ZP з^P]Y*˲#b 0i [KѰ ɀM }P!q i2B((n"Eӻ[ `npګ+ "ġ;[ cTb&[+#ç~$ Eǽ]kq$@ phS\jkH'$ܟ� !ԍ0c`3 |R߻NGP#Q'*ٞp @ i\=Yi ~GP͔;o]4Z,ɽ@ LNjbϰi?.A>VŎݵ- MMȂlȳ8kˍȑ<5>"M0 F^i�PjnGz9[ֻ\L ` $ |̦Ќ= �] c],$[!LLz � sܪͫp\B Y$m%32#~/"+*MN%Ҏ"/1]3mڭߺ":r#BE}NJN}Kqi*]"aҚk׾$ '+מ|"vx"|ʹׁ=3ʹ"͵z;"q$׶/"cy@ڦڪ<ۓR*N�KG۾ b.k/=+"<{-ރ.Mr)RJMjw!�>"J_iN_jF !{!>w|o|,>#j4'=οCN�EEv( %T˅iY^ˌr^.ɔVofkN�mi>Rv^_o.œ|̣|͟n 魰_�A@ DPB >Q"~,^7I v"@QbErJ-]SL5mę +j:}T(CETRM!{WISU,zUV]~Ȯ0`͞ZmݾW.NsśW^6X`…5XbƍDXdʕ-YfΝ]FZt_~(U"*S=Gzq8XC%N6Yd6fX>W7oqq m۸ ʛuZVtܖI4u@:s@%U.{o n$XN)<#J~9\̝�z0ʖIm5Z(Q2:eӢf7PFJn0F[*SȞFHC!sm4Qhc@pJo ՞Xz1|3N4Qǟl8ˠqe`?C4}FQ!qid] L\aFɇSASR5$ gw8iJT�V9tC~wV"=X`鲠}IGS5V(OTN@)Ti"N qC%窭IYf#l$]{nFtP ق>GA6YӅn b"EbMT cXPCJ!wYX} e]8r(9yԆ.z $gWg]MrIz[%}kwY>A[8a${ aqGV@xEp(I繁$5'\i蚶{"V�Lp[ l1 I$Y"ENttt5n*!iipFi^t!Eʈ:T2 \Ǟ_+6*{M,п|߇?~ܗ~G~}`�?Ѐ#)%JiNӳA0)D!L7uHpS&g9h2Vg Έ6YaNA=1|c(p?Y28A bv! QB9I R$hD4EP7׸F2lG2#UIŜh Ԗ(@ܑL#:ED'8ɉ�ӊ'>P$ ZE5:(e)Liꁅ ը"TjU[|ea% ^ʼ` KtI(5gEkZ DCp\r湖. .v/&~ R0 {\1ePbkT4/#+@4MXe & 55f$E i9 :OՏu)Xd V[hL0li[Fmn ,`2{qp+n8"|CDk$@ːDz#!떇v);c ^+7T-Ҽi�OWINu)۳O0WB-*wMX6D}E,,MX6(uld%;NֲHe1Y毳gB;Z:D$>5i C_\ +:^zAfHcdlnpA9nh)ErQdna[pwH;6L,u+pUy9QAU5H?Q8X#BHuKbGk\6^l؍B /(`ψ0G! BmLGUlI+'A vЃÿٝ=1uiw C.%=p;P@t# f9-iLj)cTb0FʹE yb(XvK8t3e|Y\a3UeA V;Sc *ZN ^ѫb厛1I@*]ꁜ̢) "HbV2Hvl@<Y7eeCh׽uhG_sTJyٻͶHV3rhCJ s ɽn k~֜6xX'g&N�v4bQ$*<N渚Sv\<׍x\oLҢ}hhfp #<-}j;: t( X*y}8"!bG <:.NqkyI`Vٷ'8ΥK(tE]雽Av8@? �';~V臉HW|LsY_p@M; yϓ#rF8c(~sLbS⠄qq =uQlǽK;բ9$l•$Uv<f죷?ϰ?lu{s?p@�$D@9d@i?4 R dPI Ģ\B's D< <bp粈îjl 4dʹ9 �+0/ K0# 0 ө0IS`{1J1wJ\(LY09|  |;f#+8C[@Q82)K%kzkD0;I02-2F$/ 1 @~�Ix>@QIV8IJ]8aMB[;{? j6jK5�54MæNJ4Y5V#�W[US:J6bc)Hi4KQ1i:h8Q|d:g5 cGpy8pm붩q2q[rks\v#w~0Z;~+?#D\ 2J8}|SϢBهyICD;h8;Ӹ앐c+?Ӑ^J@ɎYѹ'E1#{YA4&:3::K˱tJH[FxJz{Ğ2'-#4#d;ߨLKL�Ðwp(Ų`94X<Jz]E_K)#̼at=K˗㓾CR>3K>l>@ j@þ=C7> =YI|+#�#E@ l@v{в(?d@u 4}  �! ��,b���� �! ��,"��&���� 4؄*\J A3._A vh1$ȑ9f\Bk$9ğ+/9qI?rpܩK ҍZ̅GPg)BOU�;MV:HlمhP <n5|h!Y�,a =pڥk0^�d�ƞ K>udi�AD B!w|t^�T"䲮0h!̧ݮEgӀ7SϳA5W!üd[&,wo FA%naY,|c<(#g 4;43-6P? 8B Iș?3>�! ��,��}�� H(lÇ#JHŋ3jȱǏ CIɓ( .\˗0cʜI͛8s\Rϟ@ JѣyDʴӧPJJSRXjʵWV~Kٳh#۶iʝK݉k˷߿6È+8Ő#Klc3k9k;Mϡ>6ͺQ;Mڲ ͻwܘ} N/œ+_taУK<س|[KfW9|_Ͼ|νSᄒPM}�~"<  VH~ 8�<&h _ T�<a,~�,cNz.#d0"@ a78h}#P=7Td!CY`jd T @ e>RW�)tix|矀J PAp�y2h=*餔VjoJ8v駠*ꨤjꩨꪪnhu4@6b *pꮼ+�dզ&*�cO32dM8vv[O!{c44P 5p6,7E(� 4ﯿd�+̂20i.)x{qȤ6,nN! Il��ф D05"@ #*/ŧ}?Ѐ41r2: d)�"X B3VD@Vll"͚lެQȾ9,8/ˉ*2J@Z (z<}4PD;# Vb".CwI wZ)^ "2)[H+8x~ڽ^Ԧ@y4u�-i/"t g?q}7C*@U�.$GQ{�*B`c@", J�\4!"PN0!5=p*\np-T͠2*#*K^ �( 5S! qas�4 ʏ|DENM1T eHC)n_> oporHj,E+do(Z:xf:Dnd+ьS`bWД,"Zf{8rɶ qTQ |i�F�-BZaZ(N0P@AeNtS[0A4$ 2*gd`0&~ 8*V0m! Z)qr<e*#0 eBi4fCzhRvs㣩Bn㛺+1"d+mKA5>(@AlnX '?PS ⸁�eC Y*qP8Z;ְ"΀I [P �+*5p&P��@2&0NmA8Bgd j+7P�q�P"~QpT-mm+Ex͠lkA~Tx(�X++P07 Q&�hChWql#xw\9V` JJWִx*pg=5 <aE狀G][p�0p@ Nm?*)&`ԉ.@i(G?tj ,p1 @JEkm){8R`ph @�0 V%G%8� dSF|ei@4}͌fe8˲16�dOqu@.�GK_��!!q"csglHǩTri�trqQ85.T ₜT9 ։2 ؠF1, ٟjXM)_Sٸw)A �;F)mc,l>,bۚٗ#9(@cwo P«VN` ,C�71q�8.x (.�`hDCߡ54^h{*X2f k6T}ؤ7р:DCutǗº8t`+�6N-sNQ:{d0(@{ b?5nOy&[Quz _]F��Z�^$p4TۨQ�q*&n+P0V?  B۝:}~SֿS}"fZx|'_vSj{P%Sc_'·1�?D?ܵdv#T.v](8 [9 (@v4wKJ�{| �� +p:@� 0O 9)�QUxo `CF�J)5x � p 0 Ɛ*FJ)MK8NVfg P 6Q)0hU3Xo@R Jh  �`Y&wxⰃ8OI <~oۀp0 }0uxy؁8<TUS gUܰ0~4~zF`փȊf�:�h==B :@'V upOjP`VO3'@#B�d 0YҍSf'&`Ҏ ")LD Kd sH �]]�EF٘](?kFx O ď x?hHd��H xp9`�y Ho I0(B2D*xErK$ÔJ,P�)@01Q[ÕD ^b b `G*W.hYv)0c3wٗ\ s�[PV)>A~ɕqi(9Y, &!ٙ~9v>I0CoH.�@� ޒ9y/7~0+i*2-4.ʐI {)73;r#33L5^*�t@K3:S:W �)IAB6]3*g)ϔ =Eɛ.{YX*I*ʚb8,ܠ\ hV99 , -8L;.ڈ)X `ud)7�� 2py99*ȃ6c9A)0% D  .p>@&^ FHH?sIG p<@s@aa">Y|:AH$ڦjH'cB)àGY#y�X4=©4ozAd C*DEhA4n=t?յ]ZEc�`L~3uyBETF%V "k:%jGJ 0LuPL X�#J"ToK"EZJN +@?[;(PM[yP) # )ko⦟ OjPH9f�/\" VuVoe2)U@_p`VvWFe8@MgZ0cd__YgHk �?Xp`P a$\m p)zX@@ 0�PJAtK\+yDP³LZ Ӳ.0{.2;̵{&Shr63�j1Xvbt & 7B@ Vv)›Mւ'?1E;Vc>n%Ft_h&gw))or6=2#ZV@ٙ?TfuVzsgg+*F_fy0ѹ"sl@km&3A- LtPXZSut2Dm`mx *R; � wO \.8 ^་�vnfq:?װP�sfnVG [�!:"^ 'r[ģc YYPpٰ� (709+;}70U@f wx,xWI [o�(mt }tA+� [�q xVM{Y$A){@zb)Vʢпp'zzwG*:y+K/! P4 x„\YZ(6(MV�Vb؇ܭ`�X P�y(‡t�܈]͓잜 (�2=+ڢB XGHi8j/p£)0 @ |-2Z: gm-Wt@(DH2 &�"qzp GВ/ivE�@ yS}F͋`uY ``ǒ.)Mf0Ő'*H'X&umb+ j)8' לұ(6q='�ͅр -hyfik*&b P Ҡ @ɲӜ2}) t u0G �-+]q\!8*�o} ޫ}-M]=]}ߕ�>^~ >^~ ">$^&~(*,.02>4^6~8:<>@B>D^F~HJLNPR>T^V~XZ\^`b>d^f~hjlnpr>t^v~xz|~>^~芾>^~阞难>^~ꨞꪾ>^~븞뺾>^~Ȟʾ>^~؞ھ>^~>^~�?_ ?_ "?$_&(*,.02?4_68:<>@B?D_FHJLNPR?T_VXZ\^`b?d_fhjlnpr?t_vxz|~?_?_?_cbIx�4!0>0a0A@/M1!1 o7a /pd ){̙ 7 3#4_ο�aY6}X �B >QBH"=6(G%.49q( �Dc=DA @90Ј vdC4m'D (lqh(l[(q;eX$k!�!ep![(cIq}0Gq! x Q@`K }r7IuT D$�9є*!눮P\26vCk.D/!@4kT +jpvX8q ,otv^ϾCe nj Q`a VlYD" D!NZ@-<0A LA>+ �� Sdjl`E 7jAIB #` oY1Y@УJBn`@$1H-7PH$L&5嗙!(S~;R$c?F�p5Q*LEQZAH.UT3:o0ՔSތsN"�X�G_UJ-SR@k̈́R-2IVID8`v[T-]I]hC�k? 6qw_"u4wՋH^z ߆5!YKDGlM"$*dRI=~+-nf@ !} ]1& SYfAK1g:{1H@lae Q+4e EIXQf2|+ sLdY Z 4cqQ�>ᖛDnD$4@ fNEVPf! *#wqLzז�ɡ&�lix0׼#@-ٖzߎD'y(Z[:Nu(1b ŀ v:N"9%J0@"(Cs<:!H�:0q<G l6~0)BvD 0PN<1S$Lp87iVЀB"ac"!h$FH@Hզ>!`S_�h ".2D!Ɂ� D[ 7`h`(�� � !}i� TqaAr}%+ �O P ` c@Xj4Bx2!DF[>%$ �2L`�@.cЀz)!%Bh d%Bx& G`>ȥ gg2�B9 :SJE+NE]R䈆W(ac!ld1E�dըF pQ(dtܨ7M&ECxJD !-}Cb:Ӛ>$DCt Z*QBM$@d >Dd 7P4�2"p+]!xk]W&JPAD�G6r .β$d&5qnM("ĻQnRhK�U"m+Ҹ0/ d/ �f+Pa#(9 ӞE4� W+P 'o�0(-;[EY�U.Ы^`1@Cǐ-! 8n@淾<-jo0� j qy o( *7Z lcrz8"N1HlbYŊb1B[Y˘[s yXwYf5ߍ)OEh,@AgB�F"l�@ RC'ZWl Fѐ.,Cf�yTzC1/L_ްD +J}�ͫ08Ph}mlTL|)LחwX5se0͵8?M�%n�E8�U͊8A_(ج𠿙}Hw]m$<7([<Y N�v;7EI"rD�EBq�Sg6,j0!ސ cp8Y|n[ƀ:^Lrܩ;ISl hҞ(@zv&Y�pH` le<˴�7; �8[&h y��~LK!H%e8kܜ01�Zqؘ7fV!-őOSV�N J*tʆT5S,Tl#P�[! @Qo:ߘOg!(�(`B�?c{9P9@~Ck*0"|@ t@"x±|ª":Ga=x=؆DKjz$%Dn(# `8 P4Byl0@,0C2ė> [Gق$h];ۙXeP++C C9n` 2p�NP[H n1[aCjĊ�EC"2>9S>q i`5j骬HJپ->h>HDJT Ѕ>�fxB7#QPVR$SDBOPBjlggȢ Ȝͩ,p0Fo?dEcF }T]LJ< PA$HXb?~`cIE�Y�ċ"SSvG^ H\I0H}4ơpɃdAH$" Ȏ$-IcS,(j0{5XF"�jp�j-ʭ; cx�8c�3d\˶tzKdKj7QʤaEi@Lу!ِ%<qhCL4\*�P[$=��+/j(�J�X3L-QMԸ2@(g+ lEym(hϋ A dLXѹ b cxGL@q ڈD8;$�l�|$lD&dz0ϖMGwϜN$PENt72Ύ�PP0ɠ]9,LQ �m<M-et*4PPӲPj ܂fjpp�9Di#MP2*`&] (]+ BJ0H /K q.U�/}ң(H@8 wK Џ<e:>S9S?S@%C=DSFUGԎJ%Im(7e?X OSK P%խ0GMS-WېU=YU=X\Ւh۹ԏT_UJehijՏe2-C_ �! ��,��R���� G*\ȰÇ>HE%^ȱ#=@$S6Ϟ? O0s^2)&M{yɣ>gۼy"MJ(=5zt*U@8�2)ԂR~X@GlZk7 1D ÈHA P˘3/8cO6SclaD T4՚Ys � V�ATmܗuFm"X]5 u2`(T¸YޒAV4 n/nqg U epmSv[Wp,L@%(3B4@8 V?r f.uŠ4҈"3 5<ʼ.H uX7IciOHC B�! ��,����� Ϟ? 0s*L?{y'qEE6Š1X\I(1*p�dI A+Рyg0!є�4�b=oM ��83FBqhy03)&8_ 0aDr 6|Nܯ fӀжFLMFx5W!Y˲eNjj93lRJԔ%$?wP),+ h9r` QYAǍ n4)L^Q.n57Vu2E4+[-x]�! ��,����� gG; #Λ<N<'2Xnj&*p�dG'hP<p`$�`A c+('@hL=)p0QP�`e-1%u+ *pX|rJA]|NBLf>٫a^2N6J *qSawjÃY36Ϟ:H):_:il+OM5o.> N@gV"(E `e[&Dvj4 Q :-)} H'(@{JtePB@6(eQ@��! ��,b���� �! ��,b���� �! ��,b���� �! ��,��}r�� H*\ȰÇ#JHŋ3jܸ}`OWJr\ɲ˗0cʜIfOr7~}Q9:~!jӧ}I%\1}ʵW> UI i_@;@Vsݻ |Eʟ[X]O"sQIj!kޜ7?KLս}~ه̐!Wt9˞ݲ)V/inә-Y9k\R's|=vӝgy{U?C�ύ eG\z } =g KKHvH gN>\q'(IkehȆ%c|S(>WS@pH@=H<7~|-?,@̙ 2gғMlU.cd$$'7 S材x`s@W(7i(A M&_C[s=nVz@h%Ј#~; ?$ԏ]Q 9tE?jN9ȌFe,ژ!4K˳њ@ +hK:2/4JT-(撍J Ď`|dFKHv,yJ=kh_`I@cv|L)7dhʤ(G�2vΣ_%'OHDbtH:sƲ(FiA|ZX.}rr%g2攄 =؃d( | t<| Ӄ~xߟڛfpRKZ)-<.CW^R|Y;$6xAo7У{Wogwc/o觯/o� �'L:X Z̠7z GH  \!|  gHC8!V� P}A`@H&PE`q Nb(D" @ ؋0# ViCP�22DBPa ٷ O2"G 1|v@H@ru*�@| "GH �dHIR(�'{,*1P  >; q PC2§ |B|De/G +x>;}[Qp ݤeqK,!V\3sU|XE?P0v`;qh@@{1N5 &h "ͅz8exA*إxdh\BW! `>Ch|>4gMXTE>,A|!&Rl�t1VG |(XHap}W*Axtt�^*~ e U__ VEP)U @V KY4}+� @X K+2|s! CDԵ V@`;K fv|_슀!`+aoI[Q@oҁt HC, +(NW-v} q`d*E[xJ-,/cM& <EpR|¿�c}ihk@Ԗ S>R40)@ Cp II?~#H dQk5(@yOAbID?2$Э.=u+P\eLukeIb[@F 9vD GAA @̆/ȑFG?Rx17}4 vԤ3Ga ԓ^GY;:S>"m^;־6r${5kˎMj[ζn{Mr&vMzηQm淿 �pX�w4�DQTЃ΍|c e�4` c �0Ќ8!&GY7Op@:Q�`Mэ+h9A{C p8P�h2^}d48tP+(@ˡ.u[o{˅[QO�Z!^a�pM@7ȡ呃8P�5聀+C.XzAp_|9� ȃ:r@/_G(VaP�=�asG>["sOIG/g�` �y`g ' P� w nqb{!Á ~'p�PphC&zn0@p6@|-�/ Hp @({'N6 0 �&p!ThX`  z +i@a8exǀQe�P`{bHfH�~P~L/odIQ(6�� �P +w &`Hy؀-h @sXHs{.Ǹ،ɸ.8Xt-׸؍(98,8Xb긎(*8BHHox(� h(Y' y' 9� np Ð~# P 0 !eb n �` � 6*($? }CIA)OEy n` SQ#_U o0 n0 %9=X� ^p {I'b)h o@+"鍈)90 Z P7 ָ И`t` 0ؚ!  9xy Yi8 yt )xi ׹oٹߩyYIy!9n?�IZ9a *ڠ:Zzڡ ":$Z&z(*,ڢ.02:4Z6z8:<ڣ>@B:DZFzHJLڤNPR:TZVzXZ\ڥ^`b:dZfzhjlڦnpr:tZvzxz|ڧ~:Zzڨ:Zzک:Zzڪ:Zzګ:ZzȚʺڬ:Zzؚںڭ:Zz蚮꺮ڮ:Zzگ�;[{ ۰;[{۱ ";$[&{(*,۲.02;4[6{8:<۳>@B;D[F{HJL۴NPR;T[V{XZ\۵^`b;d[f{hjl۶npr;t[v{xz|۷~;[{۸;[{۹;[{6׺!kK[q++ë [ ϫ+@Q `! 0o W(뛻@#0 2�Ǿ@[kk '[pL� E� 1 � `|  @k [/L u4콻�Q$l  � Ҁ� 6콓 0  02,[ipP @K�@p Pq �iK; 1< bLƬ K>k, �xe` x| Y \L`<ȹ; �пs K�p o, kP`O  P�CZ2<ôgȾa` ƸlʯLP�@�-` �xLu͜ ͧ }� �  I ԰ 0 0  4l � ��@� z̽̊  ��0 �'<  &�ùK�འp}�PԠ�xF!3P8=iܠ <̿ =}4}6KmMOݽGFIM\^Ր.�! ��,b���� �! ��,���� H*\ȰCJiŋ3jȱǏ CIɓ\ɲ˗0cʜIf͈ɳϟ;+JѣH*]ʴ)BPJJeDX괫ׯ`ÊKeճhV-VeʝKݻ˗%[yKÈ{˸1տVL˘:ɡf&L̨S^zK`I $2eD*EIZ-nͼ] '(*{wL@dC` 'N?CO-w@D0vpnu�p`IP'$+QJ`3d P*I J p �&bKŽ7[0`c4:QSB-=wߒL6k,-C@˴Tؒ .d0J,2Ɣ@%p!  (҆, c6se[&#  !)R/p Z.O[d`/& G&䩨*.9-1L30 4drL2TIpW+*wĀ.ڊk+Ӓ,z@+-LD 0")- KpFGB[h*dB6〄,p,ЈJ I,bg3.j.ĕד:B@6;5�.70lLT`Tk�5J-&w*2.(cP/N(ɺr:))lL&$"-Bs` ˒wG'(~$J\( 88߆+θꄮ.-褟0" +i�I3[4.1 4z&J ] ێ0͌a2�Q*%/<W0CMv)8jn'Ԓ2e�$P$jppzg4�B\ph,0f @3 PG8N% 8Av=6zD"� s@( S@ @>C"rɠC!:3?SE0 .zfS #c d LlF'z!c:9pY8T)>~L"+CF"'Iɻ㒘̤&7Nz (GyIHFR]L*rV򕰌,gIZ̥.w^Ғ!80IbL2f:Ќ4IjZ̦6nz 8ILq�L:vg& z̧>1I}� =Ow4$MBy6< *ъZ(!юz091JҒJT&MJҕl)(!!֔6%E2d%p�Q PʴPj'*ժJiMխ<X@01a p)MmE+49׺V K+–?\ӝI2a P m)WhR9zI`DaW{RֺvŤVKI~u(46DHVx%q4` 0 @D\n �� *& 0G?@�/� t| HCr/>.T2׹o{IW@�=.Il`okG|Xآl)bSǎ &!!fLV:`HG3ڀ:Czl $C$"�}e@6t$!4 ih>?)d`돃<d۸ 8jo|I ⒰�&Q D#m~;S|FۓюtL׉S ŷiXa�1- x MB 1&jM! 2!dzq2xqƤV8 61 cQnK-\r � PǭNqNV RL+ħϞE`tW7� onJ\d+b�pIe�و.�1x&}o�Pη4},`^cOs朴9w^Ju[z ^ÚI_ `&3PyCx39M ފX镠x'-q߃Swgȥ>k2G #۸0Kyvf4Mk;!d6�@Bp?1PxuKY5P|?6ړf^&밆4t3M'7/i :=<B|*Vlc2X%A`CG TG 4YAx ;'C H^=[UWg}؇I�iwI I8|h8'f|Q.i�`P �@d P2P"I0]`~wIPrU^PUvgW%xJh(`dŅ^sU9iBxD'X�k_XOՆnXXJe|gU` @ ` qZ8pUe|  8t AE7T|xxs؉;hā؊XQ;x1#%P uI �^�QawQzTfQXQ wQ�ɘI�n(Pr0gOI 6 OI(  ttWI �Ў4VD 0J9J񘍘D ݸ߸XywEXOI-0 B�ŘOI 9sZ7P3OYX )J9J))p8XI2jS~d__~W^]}@I:s;+ U\ǵ_7@-�E]Г [I2P&�Pe\ȅIE]M8z vc^<^m`ŕ^ $V___IwpsYaE5q y)Ve` ]%eP^%ɥƛ)I(A∔bFffF{uEeVe( �V)p-I:8 LcH <vI7� 20Ėz gp�VY"9uPdGd˸]e\evvyF`IP�yIcVfgIkI��pIP� S2鹞DfdHF/:uYe]b5&9V J8eUve*] I 鐘 $ynRh4bWl@@p0k�~q'S�CNIƞP�gjI2qπ�`jjvI{*z^lxJJ��xIejl (tI{*9q:JzJZjJJCYIHzpyI��ȜAy'pqJ w~N  ��J ߙ^`p�&8SoƞpIWzI(o 7o�oUI�TxIvoZuI⺏q P� : � {mGk V&׬j4z!۬ڬMzzI=k_Qؚ6 ptzvEV~@EO]G�]t+pkXI_{IaI0'` tNu-l wԂ4#O3 e 0 |Iu;w J꠮ĵ�@ iZ` t˥IB~de;ǙI+^H\ڋ^ZD�Gizm ks�c ۽4esGy|zIˀ�wN�7}j yJ'ۀƠ�%*(hZzI J�'Fpf4>ho <¼k+D[�ud }]w4Lz["@ y篝4U|ŗt N}}��1|I&0aht +}r~W~Iz+V^6�kI[W$ P�<ol&~z@~7p}<^jn,ÛŞTa Qp̋ȸIyǙ]hFw[_jLIC_o2Xl;�bPrQI hu)-oW 6hP�Lz�I*Ȃ.xI1H]e,�Ϡ$ B�D(ʠ � =|mӯ$M#I7xo9蔟 ��)*ݑL , h� לXX 2P " 97o t | "^U ӫTf{؇$ gPt-P͇osP{`J,؃͊P$�`ؗ*WͪڰR^}۸Eۨhۺݼw(ȝ=Smҭ}؍ ]=䝥=Ľ=-Mbz}-J]Xp= Xݜ X`0k`0R•c`ѐ . Me``+}ޮP% V@ >N.IR@@� & MYR s@+ppH*_ H +@.]M(@�� %��� VؽqO( hufd75`�P��P~|LIT^X[nϭ# � ppOONOuVQnO�B4 IH� ^.P�[Ј0zA?wtKVfER %P#Zʈ�\]ON˾OY,Q^Oɀ��I!9%I_" &V �� �D� P[l 0 � IToXIW+I[5YURiOO%JsOJ)Q_sbJ)ؘ 慛ڛ?UD[wݥ^ewo_n隭 aFa&\_eyiN:󟄕I"0 +_ ] ``&q>^ߔ4|fa"ҟgcJIp4OɩIIeٌϔJ̠_*MX�?Z(Pa?DXySGeƍP2t=z0`H nD łk \B~s,0F�<D?2*z#a=B^ŚU?H} ?wESv�{i&M!V&�Ѓ6gw=;ʖl+--2�'IK0b(ZYEV0vE5#�ID3qeY �|wgg׾M[dY]mRN{-ǟ ,맀٢ɠGppÉs:Zua` <0A砸( `L12B 5�d� 7DDQDB=ngMVsL/��z6ip/8"`ul&-i`AϢ�l"&첳Kk(0맟|Dh$#PCWM e-ΊF'h%@�TX!`#-L5] cSJ@CTS+D j=-d3!9�96e ü- YǹbӺ;. (|ehϚ6-3p;o�XB@w;KWF9�<K <)F( ( ~$hD6B[P{qE& h-y�ӂE La!!6s[y>dzۈ';q|lVKa[m�8њ:دڧ_ժC};,]T%4ҷ"pNQ&Z @Qȴ&�q"$(9q�`,J5"�ܱXMk7J>r/.ig!]%�AEh@� ނc= y8yPwA|r]i FK>4}%{X,x (��(DzCA b�jg EM;K =f "sw4"΀B j!؇# %&8q#!4ρzbwxBu]Za э[(Gp. =4 7:p M:Hq8:\%,HC8%`юHnj"ɴ�FQ<QgkUK[8AJSR+=V�Chq.IiB�cπ(dQ&+lQ ಐ+BX4q0_.wr,@2T�=@g2ӂ{vu(��(�$?π=[ԑ .;@>5�2g'ղbRg,ƀtEiI:YyRE,ߡI3W~,twԆH2c&xw8oj%ǯvl2mhg=HGbS$"hl9+|E2pPt+s 6;uXCJî2!V(鎄cfAASG|ce�1Ci jW'— #aY{2�4`%vc0�j�h[-F(m~M22]McRFpz!,rőZ_h^"! Z2U#.�,Ѐ4B-aX`I�zpG`,c P 072l g2rk:80e0-pF0oohpa jV6mh� 0AB jlcHȀ5�C Ex 20Q*  @$qSzA6!HFeJ[zV.k]eXVCȠX Vt&|{* A,flB<އk�\Cyx$/9-?Y̍nuj+cY5;"D-Mh⫢8I0XsX�?,@Β3g1* IxGIp@ vG< ϗ qo>K?Qp՚zկ^4RTz( c[?~XG?=CP=C?9KB>Gы dHdqC 4Xx<1XaI0ag'cYׯN \Ppz'ӄ<`׵NW) uGBtf=ye&D[zns2'@@w'ҙDQ#T@ I[@�;J @8ڊ� 1e�|tA @@A>lt@$ӎH#\ i0z $B 8)L'i** 2t4B C BBq,:<%dB'CBB!"D=4Ae��P0F@?0 @H$CGļ/5D+NCDTDFDER,$@FA#l[2DR \CK7 1CD7T>Pe\TC7D6�MEgF)<FŰ0)lqM Obdtcm$[|GHFsWFd}*-\?,Y<@~$Ȃ4K>tG8P|AC4DvH4EPȎ4n AHql@jXFhk#ty8HҎStmHȈdI<CI,VCHtť䄄F $hCgVJIɪ"Ipt)VNؽN$\8~x˼х]axKd%LD<»w(6zC˽<ŔFeh 4AD}2MM$IJ#DaptƄ*ICA%"t�pP NND Ph9S<ό@D $ӄBdM$dOO1NGoĪI ~�J`qVlY%PEPQN[,XHXV8whL.opI< PʇYTH\|MѳOPsHs� P E O S EQU5sIɾS['ufR-mF`a8J3E o��SS\ 1-S<mc0\+x%Q$}Q".}/&m?Q'} aM6V8&SPE=Uje`IBrPP;JX~NTЬTIRzRV1S` XSA%&MTKU]U[URfR�1-QE EVB3cVg]V;cr%P}H`}hP= q]JOPQ`v_JV0yvH؅m؇S`yN~h'̇Jyp)MJ؎X8w�JPҴ\JX-؃%�[@"!ؿQY+jyx=uY=Y8ՎeoxmpL`JX[h˅ dz@\[۲ݽ:WNX*eX(AZeT]= ڴڴZVhgXh@ۋXEQXuiJy`ªXυ([wUu܀J`=́}Uإ1޽ڕM ܕ]ڵ]%X][ٴh*^;޼M|ܴxݻ [^V[-~uZ\HهF�Hp`XxBjōΔ~n > ]XS�^Ȇd`0ϡ= &] n ~�( _݋^L VZeJKbMbXI:@ F. naOaxb\z@Sci NUG=v|-H/]9V7~'D…DV 4d.^c )VdFF a"YVa2RceSVWX|0YN_ee_"ŶA_xqF؇HQnfjzRXmb\Po,t>l濁fu`OY9-c~gG摅JpIFd`Fp~85Xp~O#v[zF))7h,[;dFpVw~hJF`(g|/>hh\7nm.ff!|xɞRM…J8dp^ HInJpSV hff{Didrjix?HFeyPjWF'%i2v JJ( }~f~fކʖm[q`G[&�,eUŔF}pzhVBh҆Ѻ\k~P0jT3>b•d/ʊz辖>bh[EV Ԧm~iT&n4,n7V"N~9P9Of'j/y[\X]pnvIքw� f@ߖNNW|(.S.L׆m٦  FVpHfq�bP q;c_0B[h]Є8oLΏnrt`) ca G-a�hQFra0RNh7nxF�;,VswlcP]t+ƅ/Yq&:HrPIXs;spqKyq5fニ8ƹBG$@> uBVhtRnvrkJXFK[sO=/_0/G%ώdltleH'/`�'v_l@OIwP_P#?r.qqP:J0yxjvr_@y=_MXdOvP >cJwVdxx6 pZWVpTBh]zwۜoS{&We|WݓxPב/y wt769GڠSW[΍z}WVog[z|w|l�zZzA\]&h0x\voP? QG\HyXx[|o{m7{'G uN8={Au/IRKNb~�zSPQHPm&$~gQM@,ޯߏ|JwQh} :Q�f~/QI9NhQRV0S�I(H`)RXCH>YX)eW24JY|4ƪQX-PF`ʤi B%z�hy4xB~*UaÛ/ F`dɓ%T6mZC%IE&Q鐛Iӈ|MOa̙5yJtiSwK@*U ;ĩ`6JQFǐ ]5""I-HWN[!3_;qn:ڷP;Ǔo_ìxް?PDʏ<ǟu7O#Wg݁  -`x%(arYaeWfN9I9x~z $=P3*t96X]>tT(2"HO2?b3I^e%Mb `fO:{D6i)r{'zfRa(:cו♏Zz)*¹y )w&Je-Z*8i+V*+k *9cz*z,wZJweBJ;LN)ٚ |irKj2{aBK<@ (uu,Cy"r1+vKne m;$D̓^6lA=jE;TBvJ36$(#C[.S Q׹\sl3SI>o]-I%M�7\BvaDvq4ruf=lS=AmP>pQ8m湫8BiGu4r1y7LYQ>m^*Zu2Ѱ|+԰$WG 8xI3T 9n]~275J';RP84Î8\koAҰGDB;x?p/eI,q:ሕ8*`qƓlA$O}95zݔڵ!Y;`b;ƀCL8#X1Ado2|Hw@g;J#9Z Ik!DЊa aSlcz &X$hE1|f(~ر x(TO;<FqMI"Fm""d4?=* REGjN+B2"'(Q2]zbGj,&O4Ƌ'182/DHVQ$%$ <L8 D&?: MP^ǜa J3?1 V5G iU] \dNAPaԅ=̷ ScHKz҂ORjR1�ZA oȃ OK_;EFC!SN  Ṇl f ѣxVկb,A~80FG+FεooWIr]jS}C 8#`(d3Ǧ!f%!wTN+%�5Fzx#)\Y1u\<BV˚oU*h134HhG[Z\�8;0E! b'U+ıuJlA^qPB0<8a>:'d:a ##�;*ڈjguF�CfN-GkZT~K͇?*\  ?K-L?\L :׿pcBQ0y.R*G rN /v $dp#fї"0Of'+c Ҡ`Ce.�k>K5W SnEKh#K6#N'ߑ|AC"${INEps~@ b VX;@f2h']9/yTrzG%X5]Sl<�E#@Ah[$^N>F;ܨuA}DG p{P kGn6q!<bʮ |P3N pkcaN$)GPгǤL T<jDy?ߎ2rKm7QSn�c(YiS;!8B]MHs J3.ch7.mX<0';4BBPNN�1 G%Q/zǣmNဇacp.R{zwȿÏ'^pۍ(}q@/vQH+ y7=1g/_6xG} ㈜ZM!<r^!ѯc+,! t?Oo~Wlo~8| ]ѽG _Jp.8`_"m]_D č(A9D>( a=~4_V `IYEZ8\}EuC!,B tP&Y:oT0�CdZ$T uae2` EaT)) F\]24?2(*o� }U½Y##74;BD"L>T%fAC%LaDhݐ�Iei Hhu.()" V#Ԟ-f xRVݑDq8iE.HpZ:|V,B~(ş̰b:ĝEY6NuM5\P-;pD8C=E2p8Ѓ8TC>dD:GM$Df,8T.ErB%#PN?;>r0Af+*+p76E3ŠOju1PQT+pO+DKCCLC<eT:DMU^ewHeM+l;+dE2cW=�qM%<]hdE@6=)2H8FD3eRn$?x]r1Y;pP)=Q^e_Chbz/<VD(Y!wd%bY1jGB*ĝwcw#<^<uhBCKCUKMՌ1q:Bq(QX�?GFm1[xlpXqzo,PsrtfDSk E ŝeF ;LB$<Cgh;]^?$͕vMr>h(ܗAgw^݁FUrD,֦;'w^_ڋ>ilu9 Dld5hggDhJȂ7QxgQ觐zYNTxL6Z�'t_8T5<P?B韨)4͜))/xxi) шɑ�C8*t@jPL;NIpLҠjDlIj~gªH84H2*߭*-а|y<#>ky䪙HkA^ĸQH.T*u궖+d+kXyPv ++|+ݑToe15ȭNkK4P2l.0k BtΉpl,EhB,p]+vؓyLzRlp "j0Z 9,*jPNA.Dmxxlvxl؊l,�z+9kASwP@?y(X-REx:)D7},x[M>@ 0.Qi(9Q)Q?53El5bm켦٢^ϴVľjlFMs9tE{W|zɆlVvWr-Bz.l8PB,LiWykM%~Ve}e!/nQnVhW~l @LϮj/†N.šCqZZ]ޢڐ)/v6*Z1@=.3@Y E0MAT*yH0 N֬Ȧ юB ɭ#>x]MuaE#,&kgű[]l1]oȼίpH B,k #IkL  6` C9giݥ`ׁp<^;džreU`ur i"2pj#7>ro*b/?8#nruYcCpF1Ά!# cޠ2P53*D4bJ֯<*0*FAjo.^ZtZ, ]fuBfi:m5;/EiVDtOPFhfASciVE&Qh$4j%_ uz2i=*=״޳E --PP͝YQpJmDDJiFqܨ(ji, )NQ#KqƌbLT5N[N#W*]^5_G\ʦH6wv_'b/o(cWe__;v.fwg66iiÊhi6Nj1vP+ OMvk6*ԡ4k06kn707벪-q3tum^7!?lo6o'Bmqs71"6{7ς휀x6\,ug,l7.xm/p; ghw }/}6ڮf7jCvsLn|[|u˫7c jC[쾸�ˉlOqIG{x.΂8s',quCvgI#k#w1!>̆w"y_9g9j9~ 9o9 9f7渟뇜i;2:7::Ozh'S:g@w :z7c:Ox*wc 'ʺ9>wK7[ vH+8 }wyz/,l}9h緓4_b{7{7: +;;|{*ϻyʌ:7N}밆;~.v6fGs7xr{wo{9Gywz;Sy9C;|<oƃ{['?<O=gcwsׇ=ؗ٧=ڷ}o<ܓ:r|{߽m3:#C}vO{v|k;̳;3g{7KkS|{,к[,783G?9׼~g~գnWɻ:8tNj;wK_8/<{> 1COp3zy7@@A&TaC!F8"E/8P#9vcNJ'QTB#E D@\M6! t'S-:hQG&U4aFOFpM3 sjW_;)LgъjgA};n]wŻoŵla}0n_Ç'Vqc4v.WǗ1gּs LزgӧQV:(諓=vmۘ]vo߿gm Wys]yuNbv%Kz+j?>ow=ؗ%_o0W=So"B><�jp鸅Pp@B Il4ROD4d#S;?E} r$ PE!=Qt x<(K $$4iB/G CL+E)?85.\rKŜL&L.toO3œ! Hs0|3t??kS` 5:&-MOTjVtSK[ \tL]y%0=OUG%h44+U63"ӄtSAUGD}K$DVZ�N*\~TSBEicVoS>mw ,2?gp<nb;]AKC.䓕yVYnTi9Y]Nc'&]F_f0Y馍OWѝSvY%룊n=*{ƕ}6`E6R%l9vޱA'15Wsj Ǒs@o1n\[XM'J\Q|ob|\F^t7 ?[}lzͲqy5Xn:-LM=UӃ/>$^}m{WTyŁsL>U>n)^:hs(g o} w+sn~�-,0A JJ7Nt\ g9O膷E&0;51/` u>aaOwt{Px1 `DRMb,0nfNդEPeVƽxb4BF4iQcǮQu#RxG=%y?sȕѐ I=BR#%-I4bR#'=J%R)MʳR4+]˝R"")?[Rh/CK`b$1fLd.SZd3LhNGҤ5cMln:7Mp8$9K 9OS =ST ?о�T A P7SV/TC7PN+EbQn4)GQ%"%IQbRT"*eKR1E\ghST3iO_STC=iQ:R&KeFԋFUjUzU&T[-hWPc%k>zzUdk[VSsg]Mu{5iV$layXSfcKFvl--{XfVlgSYЖR eiMIԦ6em%]HVmm y[Rco}G UdI\Wrdns\Q$t ]j.H!! 񎗼.TM@whx߆Ҿoh+5UK_xՍ^78o<a0a&6~)b7$F1NUlP c voލ#Od!E6򑑜d%/Mve)OU򕱜e-o]f1 ydќf5mvg9ϙug=}hЅ6hE/%p!iIOҕ1iMoӝ Qԥ6QjUխva]qk]ճxlaػiUle/>6-liOگ~6Tmmo{nq+Ѹѝ\Rjv@G=oz�upnQé7g'lAr%7Qrc<-wasϜ5qs=ρtE7ёt/MwӡuOUձuo]ve7ўvmww&IY!V�[+3CQ0̄ K@Ј}(7 c!}G㣂�<p% WIa ���.y0^G V9ǑA硓ed=4ć!�  0Dh`@cx�a ~Eodc P_a`2h!��Fl�b� �l@o `zڅ-a0+/RM/r(`(Daoph_O�8!9x&T0"FX "e.|Yao8A!" @�fa*�آ@Xa( .D ֯V7' d!@�la1GZ�*(%20Q L M�@]Pð H1 jJb!X"wP0bHPco n `ā A&(�`�A~�@@`2jy8@(` aMAzš1k Af@@�Z@x#=$lA$##5 $%U&v!Ό�F�Bpj�`Palj!QŁF�4&!&Rh",A4@j ` r.o".(@Da2uR/7 �,R�d12+kb+&7r(�4�б#[FA Y%,r� "V3?r*)*�:qk3Cs0ob�g5R f8O"GqD Ws.OÐ(*mb��ġmL �γ@ &S=['&a֒ 4`vA  30S�|PA s !@fA DStE j ]TEYTBI4S @AdFFR~1#�(󉨡 * %� oB*%4?++mB29<LtM b L  -`VV�&tO&da/2POkP5+i*&-`?3 =׳&42tDK "`1GaTA$ $EuTTMͳO TuGEG'T?s@ @U]VBkB3tCm!ZAACs9SJp;sJ<rG9;]3< Pޒ&�+82@d'@ j"`^2_O 2 0 1��B`6aB.;ATD�Ah 4`N6e `e{ emfAd2 DTf( Xa dB t\G#2 ��2 | 4VVa?Pd@J6ilі �O�`~ ]nVaoVoRp Bm &@A$va22Nib4Aeov!�[ `VVgy'l'pv5�¶]xB*S7-}ckd=V(99 [5:]s/wW\h7<2)&�` @�n@p(#4S*շڗ�p�I?#&w&Cqd8 ;�4m@A m& D paj+@A�xoiZ@4aZA@�v_V1* ҁ;Ty"@AryxF ‹2!3! " �`)lX@)�xF&!82!Sf)  |�˜}Aaɖ&~�&R؃ f�x!g ͸ @ E 5 Xx GM9Q=8K5Kݕ\ѵ}9:3د~r}!+A@^ 5B*AhAy* OA-8u0dW$z)�a +w b�&ZT.:7z4�AV@yRz-K T"ly @Aw@O Rk išƀt"��nCkbǸ2I V�2 t�N�Wsqo)ï: A֠8 %Z!y+ ٟ:xwQTk @@V{Pz/|˕[Uu]ٛ'y:kaU_  � B:?ڞ aka!k^Ar/ycCVm?Zb5T� ƀ&. y%I۾b L@q{ `W ⸊!/cA<v @}p<<@߶�L22[ D:W`]<Ƈ!oyms&` B£Q@[_3|�~'Dwܖ0z|0oBÍx[Տyػz "&Z�\˕@9eq[˷{y۷ aP +sS C9�L?;u +!��im/4Cud 8Al! ht{בt*@(Vׁ=`=mN"@) p0&5˝`'0| jOY=4@ �|& ^&<->'Y&- Lt� { TMoى]�jb盽iIQ^ib >-gӳԫbquw{U~g;:ugn21 i" C<4qw$!2y16c]% @aYCR.�sAk@S[ v^m_@! B� 2#� "}= @a+YA  p}Ͽ=@M@cb B [;((q)Xv\᠀JmaHԊ4� .q $�PC+Ҍh)!"DNchJA2@ p3Ni,!l0)q*H0A*`^G  Z p`/ "|yAÉl\bƐ K\pa͊K Cc=siӬ/NztٴY +@ݖy[6�}/|9䬣o.={۹{7eSmj /qknzyt5La`d� ;|=h^Hy Wl&^h.S@.b2Ψ"Hc6桉�! ��,�-#�� H *�� \‡GɀH -j(qG A!?\ɲ˗0cʜI͛8ccư2aPHT-%i ȡa )3EEÊKٳhӒ݉K"d}ٴܤK7} �w&)+^̸ w&0DŽ|i0H�cU �7 {9pX` lyI3! ( j3aZh@E!I?2`�ej!p Cɾju&rvѣVH�'S�;@\@@@A (I&/�xEH!X1 A@Ds!%Њ-((n7(A4P8H�.( 8D]cE=͈SF)UКmM>Ce!PeDp:"@)a4V*. <I⨤j6H@49Њ@8@@Vǐ79Z,"09RlAqi@ p)3<9`<&,{cP4 0}{S(A In)A@J(@2I2g=�dnd@ -2*b7I4� X<40"& t:΁ M@WcMM?MhB4Oȃ6]8 4@� r+Œ@'XH5p=s pi@a,& ~'@tAK4Lī2H"NTՀ5ۃ@ԇ .+p]|Acq=PRA}}S=_觯,Z@NK�z,1lCPLa22kD*ijvtE�cx+;[pqn�| A (9<߂"Pl ջ^c2?X}Nb#@A"&-\ HE:$@Ќe,Z!ALwЃVĢ} p&AN 8#T!;+@hF880R."zd$'9IXDE!Hc $%e=͐�ڻ?pB\7aG`Abh �B˔A3e)B|b48P� @ .q@ :H87ٙAwz@`mr@XD �x#P4*�O+580JsK`˥)2gJӚ&2�Me۩PJԢ$Fu. лHgJժZH5tFep�UJֲhMZֶp\J׺ڕ hcAq HLJ yI`W$ш~Rd5LƶML+[N,`X NTG#Fx�'@ SL�.ئrc9B D>g܍)Xܯ$ ј,bsݢòFK�Y0A$:bcG^.w1j ŽYT{Hmoۇ(cU Z#D2|dU,XwP㔪ekTB;1!fC5 *&# #fA pd7!s"_(Y <[ ɽ-J|&[rcʈ!!�;E F%qJ4 \^GQ !Hk_ e4 h .aT "} Cqa cD`^FL*ՍX[1 NP�8%*KͣՍ`-k~�#½>r2`^+,ZY �` [P6/%Qb,P;{2Zr;& [PWB񲲙 n $P ,g|e|b vn}Bkǯ[-v{ԥ>5`f $׻淾ap7>mԦLp�t^ؤ\R+$�?G\E"mc<>}( "dv1>yvbAv̋R:a<O Dp'GP�}2 nb])1_|=6XF>^<Qaɢ7QG?Za̾A-KŽ7a{y% S̃ <-_)X&}[} _^+3y_8m5Ȼs@ X}~G�P @T~~ y {{z|G |WGy'uux7 y, @  BWwhuw0 6]5B{'r7Yg|p[;l^ABpF13܀c� Z` @x B@1P ?�R'kz%kPC  wAa+x@H�( q 8!7 +gd色(n4rW e6$q` ix\81P xDT85WC fXa燀H "` U&du`gufӆ! `x 0b hXHa� @i 1 #& _b7 vV€ v&@ Bff qy D b`Tx!Z 8B0xa`ghk9 פ ?! fDqiP jח x <gRk 56 g嘼� RhXr[ٕp ɐc{Ҕf$`H?7X "9 Ȅbw#II�I#`# pA Y!K5d 0)Ly*|hɞU  zYidw񡭕B!\eQ|Y^РY5 !} 06jy@#"Y.,jC �>`.M:a"wrFjvy <ʤ9:JC:s} 8|sBnYׄ0\pQK .;J*!h 0�gh *w DC� ,qJJH~ZЕ! xcډ]bډf>Jz� { X }s. p"?3 Z ʠ �B}z}8,ګ֕p! *:N*kKIb AP*yA .CAv$*v ČXȭ#  >a 1.� 8 x z[ [M  @رW @ix ]iD) |Gv J- * bKĈ p(DnJطc` p_  9YJ H K XcpD U rKH @gg{i"D@ص[;>ay; 4LpC"`g}Y[v'+("VkFk啑S \dƠ pIG([0fuo9 /<kvkp ]NlC2  C,g>iȫoB\ DL�;t* a FśPxKX2W+í9q_ Z<piH7 Q%GĪhv7 9õkXk@xlzX܍(}l{ n#{h@}LLx[Ɛatư`Lrq u\vwWD֦Ȩ q+͉#͋Ceb| Z͐ :8>yyf lT�\pJ} =]} "=$]&}(*,.02=4]6}8:<>@B=D]F}HJLNPR=T]V}XZ\^`b=d]f}hjlnpr=t]v}xz|~׀؂=؄]؆}؈؊،؎ْؐ=ٔ]ٖ}ٜ٘ٚٞ٠ڢ=ڤ]ڦ}ڨڪڬڮڰ۲=۴]۶}۸ۺۼ۾=]}ȝʽ=]}؝ڽ=]}=]}�>^~ >^~ ">$^&~(*,.02>4^6~8:<>@B>D^F~HJLNPR>T^V~XZ\^`b>d^f~hjlnpr>t^v~x |~>^~芾/>^贐难>^~ꨞꪾ>~~븞nM>^~Ȟʾ>^~+@�E|N E� ^>^~�@P n(�0 }~Pi�ː $_&(*I` p IP�ak p|砠�۰�Oo R?T_VX|LE [� V J �� � @?E�;cޠِ` 90�Vpbd � PE�p n�- �k/oF"�稀�Ұ_ u� �{ rO԰_؟ڿ| �[ &*m ` � �o�a8qX)A9qV4%-D +q[2% %р zF*%nfV"F,eu"lŝ=6|q"A�*V" 9 *pn\ 5r�&NqZeԀ8( |B^MX`… FXbƍ?Ydʕ-_<9-{4Vd�\=Y`8֊ =駣@.V,�+nC0Xw,NXS}RqEr_!0VQS'8>p 8k2q8=/)-˿ $`4ġmЌ8ʬB /0C 7C?\l*�P:#Q�\jk"@6Q$HEqpi��h@]�Kn$�yjWFq@*`$)CL�p*O2<$4NV&dJPkM3qa( S�Gs$+q&DK/4SM7SD|j�P9Gq|DPO)iha#%I1ʊlJY>qR)BcJڽTX"eB=2n +V�+XNADZSt6a)_8`nԊ``ܜ3C1@ Iu ֯&4c$_CLBN8~b,※  -H&d™P 86�+ Lތ$(“)`;lk$( Є @ �*j:cimg|mnbߋ2j#v-5EzBpelI#-hJ==<+ G\/k2xCw]=wiB$ Zl矇>z?bH("iqقF ֘-Q*E)A`kqy4>lr#PS z �%H/L_(�K(&xy5 �)ovmz?b(p͈GDb$6Ob' &(6D.vы_P8F2шN4c(?8L)  `cGňQHB.Qh a8` Z"%9IJvяd&5IN&1e(E9JRҔDe*UJVҕe,e9KZҖe.kI^җ$#9LbӘDf2Lf6әτf49MjVӚf6Mnvӛg89NrӜDg:չNvӝg<9OzӞg>O~ӟh@:PԠEhBP6ԡhD%:QVԢhF5QvԣiHE:RԤ'EiJURԥ/iLe:SԦ7iNuSԧ?jP:TըGEjRT6թOjT:UVժWjVUvի_kX:VլgEkZպVխok\:Wծwk^Wկl`;XְElbX6ֱld%;YVֲlf5YvֳmhE;ZҖִEmjUZֵֶ-4 r7&7��| 3M%̌<+L*s|5`%D3uoVטɅf�mz�6� +D`VP/g:bN� @,CTaA3k+d„D 8X 12L s.01eP b7`^cm1+aט-fib*S+L8F "b Mw2P s2R4_ט�o0a~LA&+ehXFf戳j[|Ia ff> ��0!e0 +YiLp I Gbl91,Ld#Tjjӣ4qVl02V7غ3 )c_5ZP֥m W3\&'�ۉFĢ+bV` (aȚ@�'|Л2WmUj�@$gB_(ܠ�.f` qrGk`��7 ` "gq@XP& S@G� hs ]-I,LN脰` C_bExK.pC"P v{sםO-ѷVG՟r9@ZmAuh4a]BD;y-v\5:Gå#į�Ww 9FeU p9WCD; [)v3xUp+" s- w"n^j+hh@Sq ؊@9Xx0@`4ٛyHx`! &(.@aH,+Hh aZ@x3 PP�# @ ` = < rߪ `ū p <`A hcPIa pW!�,H <iȷfI D{lãHm(3BîЊ˩ abp>[. ٢2�H>S d 8[!BK` i+ _H@'ˆ yJ�PE2P: `(` @JPk.!SܰY1iB8c�c >p#P kaNJp8J23HH{pqB.zĭ&ٟT8oX� pHcHq"0܏$: �z 2߰1Akٶ}4G(HIp$�x9lHXHC9r-p� Ha1(. O X:HKS H6Kk&q4IКaȮRV hdx"HbB! j#e;d h!=)>QDI-'\!!!�S*--{Y6TS$�.!�욅 njN$�3!1.#>a`,T�px5$hX� 0 +Ͷ1|!i�a p+nd0|$ԓ$&L%M5)5PyE2x\ x0XO,7<0nLfsmV@Ź<K+N-p�p?aZ�X 5Jqhh(�aD K.<* ](ae!�&d MC7 0CEMNuaf&hcAӗN `RDNPHaTaP`Gٶ'Tt8X �PȖ'.C=>ݕ^qbYX � UJ,cU_Ic48S:V0p#&Q0tBDJE}I$HQaZR|OR[RRn`Je ox2,!hp�i3( Kq;d0tLuI!)4GhY&De5|F6 x0ISX5 Ie0Q}NcZajВDcUQ}RȧUU!Iq IM8$8.ygL`69r5Ԍِ9R'%ۤ;m*bYe!D[fs2wM 8z͵,�7׶ 18ٺþZ*j0J_k]bT2 �YHL؂QhS@I04FI�Izl ҁYe݋4ePX%ڝ', i1}_!PԬuŝ#C0,`[I[x`0`1p) 2KU I]Ix & Ɲha~DYhqc]N ` Maj4ލ4hbSS7q3ߋ|7~#ҡ E*oc�s?zOe40Jh^#    ^դۑT=8T !M�:O~YZv{_dvZ jI#Zf3ޞQ& X IV-!څY9�Y&_fdVP�~$�SP� J._yD1h�d&BYiN!_VX FJne` ?gS2bP`!a-h*xڡh#ӡm2A12H8v+',&FBibrqipJEXi-+?0rꓮmf~N'.j& @ "2+;ꤎ-6EjG&J+.&v ��! ��,b���� �! ��,b���� �! ��,b���� �! ��,b���� �! ��,�O�� H\Ȱ@��#Q3jܘ#ǏYq# zL 2 Xɲ&lɳϟ@ mx2�s8,'Kj659pCbJpkJR-U\)3^:z m#Ώp˷ߞEH4ٌd˸ 26-7{Mi*%�7 Q֯c 7H@\(4XO1ɋ `kز K` e1�ʼci~$0"z!&6|i0@ͱT+ o14]uao G\Dr9Wd.pfX`1vaLڌǞ{G@=W}^|B^{@H ]C6fX2`8@+ec6ؒ@b8٬ЇcE'P^)@e. ǐ Ïi" Ќ8e!K*UGH! jfG'kcQ$'v+sy'9j& k*F8bhVziyp)Z:Jx#U+ԖV@*A@˪aA@|0 .$=�?j@P? <�m9Њ@8*Ϋ*Q�#A p{ 1!d3:tCO3A֔5z&+r36%t,S=@р 䓌-,ۥ&8/'[45 Ǥp:7I49119�gP@�Z ΅QC4 L=�J �nc?8LO^HGG�`^J<knPm/)O :Ԭێ;' tC#�*(2P=o�L Aa@<*b [:G 4<qntᑄ@ء: K}',?* `T~"83+ O BĠw(9z� s:A5L!' hq5K7/C@fXQtr1cEԅ^S>K/3 JPx,0챎1DQ�M"  3ȼbd A갆4NծF-�@YDM 79Q^%- cbn<nfm eh2eDe8 JW) ȏ罠`f4".tSB銄| 79@@C +0+`)  �€@�,!a(;4*T,$6}Ё =hB ІRTy{ԁ')M5`S}6D5Ś7ɱ'M@ЊRƭ/]Iۊ9<Wȣ h@Up�v):<p ��2d T@h48Pl9{Dyp;@P$g^[DDxx -b 2 GdU-]hCDױdIiYj.u/J6Cˍlz e�pͲdml [-뫀L+QE;4AjPQp7{V '(NW0gL8αw8#c` ?Jx-jGNMq He4(<| 0B-d>H4Ld9-F7Cɕh;d$c]d�E>e3k\tO y#|A px@ᴧBhZ <1J1}ȩG轔z#*g,DӒ6KBD%lq'dC~��L�X$y.P>f! T$# 6A=BD+ dmlk�ΆM�${ٯ&� $#f}lf@ns[ nwd@<|ȢFyq~~�>p$'.a l޷@q JX|r08A TL T2W?G8mo{AH׍n5eB(LC-oػCŴa v�8% S̃{V(Qxr ↉ dqbhE}qw>2ZT;7~c|g7?vS47<zǭn^g>@8y~ P9]Xb D=*QlԚ q8|cϨǼÍyPWh0 @w}q 6ww{y¤avg{ gw$xwiP {1Hi`INIGnfp3� HP  D�@M9(;p4hЂ/18Mh 84P冃:\(^o7AMA5Hاr` @pkzXp._H�� r8'k eՈEh Fw?w8 XHm(gs8 ` (0j%e�9sHp 1 P ^@ /XF6ې<a Wn0xɸHp$(P ʠЎ=G 8VҧkHp эѧ� c&6p ڗV8- d&P 8H�H"IY9  h18W^6 `.a ȑ@2&pe'Z &ߘ\ EېP5xxIzi)QlV؇ `{sg|iUym֑)@iVxlf!yX  !2 dvi5quIH@R w; ]I�a � X妘Gx8YQVw) Ђy pyWfi� }6}q/8 )C ʀ `�`byu! �QI*C1jة衶Yx~InH I  dgX|@ 7 f} h4٤zLjHnإ!@l p҉`z+uI` X!*gId\PD 0 h7tj7 8 mEuqi- wy*�z3-Z?X :NP*H + ^B*p3 0 G QH@K+r|u笕�n9sȪia =qozW*+TBsKr%|{h �i0 p0% ЉzI�琑 x"'L0 0 5 *C +6k3ƀ @7iF@C[pp#}A; � 4]bX5T۶lp+n{xz|۷~;[>'!MQ 3A#[5fb_qE !BzB�^QUr>lAQۺ ېI&4W2|Qq`c�7q-$RǑ""Eٱ1Z!!qW0_"[ $?R# 8:$TDrT$$P"%;`K00*,B~2&2((+‚D P c2½IB(".Rd-"ÛjÓr"/5|JW I#4 CE4 S1#2>ZnK3:Jŝj#6Pd5@2els66r66p?47us7y7K\ķ=c<S=q<C�G1:3 P�(_ *B;d-�>˳S;;|?6:@T@<{\DCDAEPB3$A&ɏKB-QTKL& 0 ͠2ͪFc^f:C,k4dC8dWtdGzD O\XWTL4IʔL̄EFAiI<#cLL(d iM�Р$N :QN$_MN2O+{AT@EP5U P!UQ}OQQQ H"  ТETԟQe(lVL5aRgM~pes%f6x%z5t@K`XYߵ]E]eXUXu(Z]! {Ն2p:]Q}ZP_5% _5_]Ϡ]Wnm\װa1۱}۸VaFۺMa6=ĭ"FbW:ɝ]b=]}؝ݱe\AeVkW@z 2 ­}/GpK}'tth&k@j))w ' x� �a6@n r q; &)F (N/N\l@q@Y-h, DmCf%  ~@ P~ <"%`uA pQw 0~|1) jq 0CGJ`Hq1uU'<stNoWh]h!'tn]1ujGtlvvdmnxv.tumvqe7S_@a '7n6s:'7 kpnΙjXؾqWqr2Ksnuq QW;spuGKtO'(Js+G&7z閧DHN @8Ν|yg.!*g'F߀؁AL5y.J m-ηU*~~ Wl0 l: *@ @^{ ~2o&kD `Cp{0 iXXLInϑQ�p"M}A/k{DHMЉ&a<* Q]=ek?x 9^0 z.) .d_?�A�hH,X>a}9ՏF±3 CA$ M$FF&,1@XLZ>6 0u 9m3I A2qYԇ*gY/ 镀U0;GUfI�*ĉɍ*~=*v$ۇ UDV?Yd/q\ UwN%++w*5j*a߿Yl&!,EȞf+ <E ".aFmR׏V=-2:e{Naq]59ߨfv4yG7!`d{)jmlsQ@!ZvA#@G4zCDNvi?21Ud1.[ @I1wDӑ)na : q|ED !|h4sN$3LNKm5 4ȼ$ !q*7=f[𠜸oj!Pqz͵5!20!#7~ ndӍP!qQQ=h;1G֍gαLE{T,ToT] ЫJl #JZ\ڬ )�uG+p\uF%ݼqDyl]wW^I7FLe ~/mO%$t-P& _UZlKK@ZGcC3S:7g%⑄%!wmuNe^Ǒj鄞he K'M+T”Ǫ]~`Q*n%u 2>YXt1K VP#`AEeP{$%�XmQ,}a\vjtJWOJ+w)RŅqf%[,rR*m=z쵉ZjZokg嗟g7zJ& pާ"HG#}'^FSH'U-Z*qQk{28!z%Np"\`1#VQ`E~CB?P;HDQ!0;98ܱ N�2+8D0Pl)qQ8(,0CFl[CH4F8BEbxGqqa6ޡVB ņF1,nI%!w� !D>B*M-"|Jv !<@4% RP �=1Ә#Erl8eS‰Ft�x+1@v 7؜/*=]0Z$FaIsP+ yZe2*$!*Rastu˱4g .8!cM,h ӁڇiX c8b$F $C-e . :TvKzӝ FBgbN~qVb+{ZLzӒ΄4GTy]-J" jZs$3MPh$XA:Id"/DlbX(Éld%;Sp:L<TѬ>;Z8EbQ}HmlewcՓR[&Jj[Z׸Q()6׹υnt.wnv]$û w;^׼Eozջ^׽o|;_vı^#,G m+^cQ0'27w`pª.v$Rjdɱ2"&qY3v43L!f6!L2|LJTp#Yg 9dyJjK`Qd$z N-'8%Ŀis1eREp fx.~LgbCѰ`XA vlc3H4ohxϑe T $@8$ vCJ5Mi"˨uBQ*s[[/3e~>~8rV9Ow(N! NP壶䤊~}ȉm֏J%:+ (#9jȊa Y)89g:Qt{?0Rr9$W"Ox% eBd .qH<Wa!ȘwѴ@-NKrf:9@t1GqW?$#҃ |5 ?a-`\۸@“U[\پvB._<yVbO7,֍p: >$7ɱfA\qZ0@^ǐ)1_ 7K͝QJH3{'яV)L5V\xlgoEA Cĝr2AL>;9OVP0#d/(OhjC3z � \aGShK V  @``@\ a )E?ț<Ⱦ У# ,D@D xA?S jyxy398 J<3d7Ii`#_@%T90пyv`<C|]hW:ç8@ |`0L`Gq-3tį9ȘI85Gh`2i 4Lc ȈH?1Hh:aF0Ƃ*,$ &BCKL.t 0܊2|f JD\+GE 9̋x1 8q$QQi'iЀ7ADS4@@79;!Y$<a,Qx>�|PUGh�ǘKȏ.A?D-9. iAY,((x iHGp}T߲J0 ǎTK1 l@y~<*4JG{| 4B`̝!DDHePhEtWؘ7͊�h8ĄȯILe^Id`X}]93a cdeEޘnmAΐ�rYts~zvyx M|d"y'dOX̌! 99MLNPlhǤ+)A[ć !HDm!Q؅q 9@҅C`?򁄲DQ9Ѹ"Vp༛aJ t| ɜoR )\ 3USQjёI0Q"F %:!05F'COi,ѡЍ8b A*$".r țJI$K:LZYO}$TWUH{FUd&a"&Vp!y@b=V I!T\yLeVLVP:T%{:!~ڡ"!""bH%j'r0`)L^/ QbQHwX#f`DX= NH؆]*?5$B2$1Lnb$Y*4UR$muKSfE 51ʥRVTjTb� Xbuѥw "p6%0+ݱb+LĪ'p«Ѕ?=,,8,oJ`c8I7E[ʼnMjmMx⧸[:(NЧ" r[[Qw٬((P٨*۲t -ZB)z u)"*c,e݄0JZ勨Z*wUڼ*+sFݴz~#^"+Z}5w0,Vص "@в:.e}�Gh:__.ߞ-B�F`0JHD-a} .&6F؂02ҰXaz.${+Քca7LabY@u;N a+`)c(vibbŒb\#`]/7.aw3,Y98<& o̥P[㋭(VchdYc Q`dGv >N䲻8H鯼;nI 8uJ@Vچ{;<F k;;呙jK;8V==?SB!\]X=KZyhSN>"Yqq.gk6ȥfV:w�usI>f^0�<Ha6B$g&tBu,eiVd@G`J@ Xxh8,Fqdp"X�rt)0;i1si79| ŰZ^e0HA$C||FCqfFƥ8iPbǐV fHE$˞ [0X�yq!eXYLN*<9lnk$ʤ2VN$VEM {l/)I<>h\Ֆik zjDn~ mςpmmm~b;LΆL\݌Dsbn玓כ=P>dFEVnt=]i08i^8o;oY@ 6jFPGm+ѬMo74 p�QT@ nHYT(u_&?NX 0A$OYTH GEC h~CVi VepAYWXU}U^ws FÑ [('&*&Ud#^"h~"]>^Cw륟:GxuDjۺtQ^zsdm^+j7*.>Z.=Y_w.{Xa'.ׂ-cfghG.(`iklmnopqp@�P���^vJHzwwgV?sGغ$yHX2Nx7nߓa�ɠwQǨ=oxwgIXl0BBWɺy  1Xwz>ɇ` HV�P� @z/D�9pX9_{�vgt'�O{( ~hz [`yO  `0{W{{ gw(x XX*{/~{oyHX>xP|'1=h�X n@�@ p}ؗ}Bh(*~X�*}/ҫp~hq("0" u!w8{·_Ta (h JI�n =GiئV6 eK\} aɓ!h&LePp@œ9ʉ1K׌�K0 &}Yae`ҩAD7ct"sjײm-ܸrҭk.޼z KA9 TD\߂gGdh�b&Q6 7m23F97Ÿ`J* @L|ncL=ߕ IK`ޑ[&፱J%vӯo>7+Ta]B[ hP:�8 5PÀ UЄ^hPr~n @> kirmZxmqDV"VP:H8�S%aq1(A 4  %MHA��*DJ�d [oq9'uy/C@b9zP4,c ^A*hALYП#PR?-D؇qtPuC,;("Oz9�S'JŤw#ɊP9S0p%�A7TIԏ:ՇDwj-z-^�@u`Ј0-Zs1N[6, HqSΊ{>DSZձFzt Yʉܕx ].936Ȱ(ÀdCHAK7b�s0֝:pPIA[w@DHJ zCAmmP> oL^=CP? Hō6 #Vcm][CNzurWg]֍+Y|\,3~{ψp[32 "4S�uA+p|A,RHm DPǞԋ`=eDO$ ||{A[@y+X�dF1T $G>/}{^'$8u\6JЫ`H'- &Pu -;! S b! c(Ұ6=vQݰ>!(D"%2N|"()RV"-r^"(1f<#Ө5n|#(9ұv#=~# )A<$"E2|$$#)IR$&3Mr$(C)Q<%*SU|%,c)YҲ%.s]򲗾%0)a<&2e2|&4)iRּ&6mr&8)q<':өu|'<)yҳ'>}'@*Ё=(BЅ2}(D#*щR(F3эr-B*ґ&=)JSR}x)Lc*әҴ6)Nsӝ>)P*ԡF=*Rԥ2uCl*T*թRV*Vխ”>+@WDbYsBV!:W*׹ҵv+^-mAԲְ+zV km,TE:^3rRWİk,!ɒqcU)g,�0 򶷾-pj �+J4 ƒ" JkY KX.ύ8ʍ-0@+q@T` @+! Ѐ/~o x`*pBͽopZ :(�ǃ#<adÿ0nu&>qg�Y 4fCfl_' eBz GxSd>&p`n! Z#~"0 e"M~r 4BI� Xa  F� "2mC3/+h`A[ю~4#}KP.�"ψX(ְinص HA�Tһ60 u#AX}~`ԁap " Q&N{S�*(!qnpgg-`8*Yx0j! ȷJV_pX �Ŭ-v�} 4�P~  ( JT]�b冷 V69sn6Xp8d p8p p^2808r[8�VT w$fCfj\{^�p.HpQs?QyK;G R˿N'�j S鐗<�{\_ץ]פѮv@ɺ5} 3<⓯/_gu`4 jlCGF.v?||R6v,o^vsd|f]-B!V8�'(-1CE&�7p]u~9 4@0�H(T"I 6]9|W_q|ؒ]X4Z8l!tPC0@P[ ơV^աl a!!aNܡ"#6#"!=$N"%!&f&n"'v'~"(?))"**"++",Ƣ,"-֢-".."//"00#11#2&2.#363>#4F4N#5V5^#6f6n#7v7~#88#99#::#;;#<ƣ<#=5rC(}jMYW^X^[X_>?"kCEDDBAIE*@d}EmuHD,D?Zdm\dAZAh@IdG…MĢEEfF6Q$}dQIR*eQ>dS#IVI.\< E"UNTY ` $Dv}[XXeQ~CO[ K…Q]\h^*ZemVeKUv%@^bVb*#Q@^@|%f|]XIXi�s98,Ws�<HZXf_e"О8p$dZqWb&ifNpuaWjXAtG$جŁYuʚ80eu@䣂Z_e$XW{`_6%{g{Hb֖}~vb}[H&SNdReQ:XhIN& YAd&qYr]hٝٞ،՘8(IBAAșkl֠8PYF؋D؏Zߵ%[6aY Z3@)0�I@) `(K<HJF%zVcgXF6(~B^'.)VFҊC嵝i$ڊbՐCX%y|DZLJYmjAP[AH�MR ٵ%YQ[x5sN 9D8�00[g+)6f*fH"h}"(©6"jdgTVjR2NҤqX=A׻ۼhU.8B4��^ꄿ�4@P­u, kNM>!V#Lm&VDjI@.B@\k-~$jҩ §)6Zed&Rf'RR\fh]m]N NJCgެ{9)J[C!V.BD1>jl]A"*vZ4(A*~8\mU64f-nfU:lm~Zo)T-6`-HjN@BA,�)}D]C@$m`U"͛8X͆�'嬰2oQ/@fa`iޫ+VB+^/'Fqc%ᢥ?1z^B])* JY3q]_K[_q*0 @C>ꡱ#DqYcmO TfD� oq6$@ 2~1@# �;*)'Ug'- rz?jr/-%ad�(d&�Η{ rIs h10R �,Bq 5 ʠrJHLAm6 o;>;"_Bs>q.'0k-)/.**r+iz//GCvNo/CJS8P�'% I]4ZGLoKMM6I8�PNfEu^4N3u5xa&T?R@��! ��,b���� �! ��,b���� �! ��,b���� �! ��,b���� �! ��,b���� �! ��,b���� �! ��,b���� �! ��,b���� �! ��,b���� �! ��,b���� �! ��,b���� �! ��,b���� �! ��,b���� �;��������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������deken-0.10.4/developer/�����������������������������������������������������������������������������0000775�0000000�0000000�00000000000�14764245604�0014663�5����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������deken-0.10.4/developer/DEVELOPMENT.md���������������������������������������������������������������0000664�0000000�0000000�00000003732�14764245604�0016774�0����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������random notes on deken ===================== # easywebdav (and easywebdav2) To upload packages to puredata.info, we utilize the `easywebdav` python package. ## easywebdav (Py2; Debian packages) Unfortunately this package seems to be somewhat unmaintained, and is currently not ready for Python3 (although it works fine with Python2). If you are running a Debian based Linux distribution and use the package manager (apt) to satisfy the deken requirements, then easywebdav has already been fixed for Python3. If you are using `virtualenv` to provide the requirements (the deken-script internally will setup a virtualenv environment), then you should either stay with Python2 or switch to easywebdav2 (see below). ## easywebdav2 (Py2+Py3) There is a fork named `easywebdav2` which runs on Py3 (and Py2), but (as of version 1.3.0) unfortunately has another bug which breaks the `mkdirs` command on the server. The fix is simple (but requires patching of the package sources): ~~~diff @@ -145,7 +145,7 @@ def mkdirs(self, path, **kwargs): try: for dir_ in dirs: try: - self.mkdir(dir, safe=True, **kwargs) + self.mkdir(dir_, safe=True, **kwargs) except Exception as e: if e.actual_code == 409: raise ~~~ # Windows Standalone Executable you can build a standalone executable with the following steps: ~~~bash $ cd deken/development/ $ pip install pyinstaller $ pyinstaller deken.spec ~~~ This will give you something like `deken/development/dist/deken.exe` (The standalone installer can be created on macOS and Linux as well) When creating a standalone executable, an attempt is made to automatically fix the 'easywebdav2' package. # Recommended backend You may see the error: `No recommended backend was available. Install the keyrings.alt package if you want to use the non-recommended backends. See README.rst for details.` This should be safe to ignore. ��������������������������������������deken-0.10.4/developer/Dockerfile�������������������������������������������������������������������0000664�0000000�0000000�00000001635�14764245604�0016662�0����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������### runner # Use an official Python runtime as a parent image FROM hylang # Set the working directory to /dekenserver WORKDIR /deken # Copy the deken executable onto the image COPY deken /usr/local/bin/ COPY deken.hy /usr/local/share/deken/ # we are runnign as root, so set this ENV DEKEN_ROOT=yes # gpg-signing in the Docker-container is a bit complicated ENV DEKEN_SIGN_GPG=no # install all the required stuff COPY requirements.txt /tmp RUN apt-get update && apt-get install -y --no-install-recommends gpg && apt-get clean && rm -rf /var/lib/apt/lists/* \ && sed -e '/\<hy\>/d' -i /tmp/requirements.txt \ && pip install --no-cache-dir --trusted-host pypi.python.org --upgrade pip \ && pip install --no-cache-dir --trusted-host pypi.python.org -r /tmp/requirements.txt \ && pip uninstall -y cryptography pip \ && chmod a+rw . \ && rm /tmp/requirements.txt \ && deken systemfix --all CMD [ "deken", "systeminfo" ] ���������������������������������������������������������������������������������������������������deken-0.10.4/developer/README.md��������������������������������������������������������������������0000664�0000000�0000000�00000030203�14764245604�0016140�0����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������# External Developers # You can use the [`deken` command line tool](https://raw.githubusercontent.com/pure-data/deken/main/developer/deken) to create packaged zipfiles with the correct searchable architectures in the filename, for example `freeverb~-v0.1-(Linux-amd64-64)-externals.zip`. If you don't want to use the `deken` packaging tool you can zip and upload the files yourself. See the "Filename format" section below. ## Get started ## ### Prebuilt Binaries If you don't want to install Python3, bash (MSYS),... as described below, you can also download self-contained binaries from our Continuous Integration setup: - [Windows 64bit](https://git.iem.at/pd/deken/-/jobs/artifacts/main/download?job=windows) - [macOS 64bit](https://git.iem.at/pd/deken/-/jobs/artifacts/main/download?job=osx) If they don't work for you, you might want to check the [releases page](https://github.com/pure-data/deken/releases) for downloads that have been tested by humans. These builds are snapshots of the latest development branch of `deken`. On *Debian* and derivatives (like *Ubuntu*), `deken` is also readily available via `apt`: ~~~sh apt-get install deken ~~~ ### Docker containers [docker](https://hub.docker.com/) is all the rage these days, so naturally there is a docker image for `deken` as well. Get the latest and greatest release: ~~~sh docker pull registry.git.iem.at/pd/deken ~~~ (For the more daring, you can also grab the lastest development snapshot under the `main` tag). To use it to create your packages: ~~~sh $ ls -d deken-test* deken-test/ $ docker run --rm -ti \ --user $(id -u) --volume $(pwd):/deken \ registry.git.iem.at/pd/deken \ deken package --version 1.2.3 deken-test $ ls -d deken-test* deken-test/ 'deken-test[v1.2.3].dek' 'deken-test[v1.2.3].dek.sha256' $ ~~~ And to upload packages: ~~~sh docker run --rm -ti \ --user $(id -u) --volume $(pwd):/deken \ --env DEKEN_USERNAME=mydekuser \ registry.git.iem.at/pd/deken \ deken upload *.dek ~~~ #### GPG-signing with Docker Within the container, `deken` will not attempt to GPG-sign your packages by default. If your container has access to your GPG keys, you can enable signing by passing the `--sign-gpg` flag to `package` (resp. `upload`). The following assumes that you have a properly configured GPG setup in your `~/.gnupg`, and `gpg-agent` is running on your host machine: ```sh docker run --rm -ti \ --user $(id -u) --volume $(pwd):/deken \ --volume ${HOME}/.gnupg/:/.gnupg/:ro --volume /run/user/$(id -u)/:/run/user/$(id -u)/:ro \ registry.git.iem.at/pd/deken \ deken package --sign-gpg --version 1.2.3 deken-test ``` ### Manual bootstrap ~~~sh $ mkdir -p ~/bin/ $ curl https://raw.githubusercontent.com/pure-data/deken/main/developer/deken > ~/bin/deken $ chmod 755 ~/bin/deken $ deken This is your first time running deken on this machine. I'm going to install myself and my dependencies into ~/.deken now. Feel free to ctrl-C now if you don't want to do this. ... ~~~ See [config.md](./config.md) for deken's configuration file format. If you get an error like > -bash: deken: command not found then make sure that [`~/bin` is in your `PATH`](https://apple.stackexchange.com/a/99838). #### Prerequisites `deken` requires Python3 to be installed on your computer (and available from the cmdline). You can test whether python3 is installed, by opening a terminal and running `python3 --version`. For installing (and updating) `deken`, you will also need `curl` (or `wget`) for downloading from the cmdline. ##### macOS On macOS, you can install missing dependencies with [brew](https://brew.sh/). Once you have installed `brew`, run the following in your terminal: ~~~sh brew install python3 ~~~ ##### Windows On Windows you might need to install [MSYS2/MinGW64](https://www.msys2.org/), which comes with `pacman` as a package manager to install missing dependencies. Once you have installed `pacman`, run the following in your terminal: ~~~sh pacman -Suy python3 ~~~ ## Show help ## ~~~sh $ deken -h ~~~ ## Upgrade ## To run a self-upgrade (not supported on all platforms), simply do: ~~~sh $ deken upgrade ~~~ ## Create and Upload a package ## You have a directory containing your compiled externals object files called `my_external`. This command will create a file like `my_external[v0.1](Linux-amd64-64).dek` and upload it to your account on <https://puredata.info/> where the search plugin can find it: ~~~sh $ deken package -v 0.1 my_external $ deken upload "my_external[v0.1](Linux-amd64-64).dek" ~~~ You can also just call the 'upload' directly and it will call the package command for you in one step: ~~~sh $ deken upload -v 0.1 my_external ~~~ The upload step will also generate a .sha256 checksum file and upload it along with the dek file. If possible, also a GPG signature file (with the .asc extension) will be created and uploaded (but you must have [GPG](https://www.gnupg.org/) installed and you need to have a GPG key for signing. The GPG signature mostly makes sense, if your GPG key is cross-signed by (many) other people). ### Creating/Uploading packages on a different machine `deken` inspects the files in the directory to determine the target platform (rather than just checking on which system you are currently running). Therefore, if it is not feasible to install `deken` on the machine used for building your Pd library, you can run `deken` on another machine, Example: You build the "my_external" library on OSX-10.5, but (due to OSX-10.5 not being supported by Apple anymore) you haven't installed `deken` there. So you simply transfer the "my_external" directory to your Linux machine, where you run `deken package my_external` and it will magically create the `my_external[v3.14](Darwin-i386-32)(Darwin-amd64-32)-externals.tgz` file for you, ready to be uploaded. ## Filename format ## The `deken` tool names a zipfile of externals binaries with a specific format to be optimally searchable on [puredata.info](http://puredata.info/); LIBNAME[vVERSION]{(ARCH)}.dek * LIBNAME is the name of the externals package ("zexy", "cyclone", "freeverb~"). * VERSION contains the version information for the end use (this information is optional though *strongly* encouraged) * ARCH is the architecture specifier, and can be given multiple times (once for each type of architecture the externals are compiled for within this archive). It is either "Sources" (see [below](#sourceful-uploads) or `OS-MARCH-BIT`, with: - OS being the Operating System. Typical values are: - `Linux` - `Darwin` - `Windows` - MARCH is the machine architecture, e.g.: - `i386` (32bit Intel/AMD-compatible CPUs) - `amd64` (64bit Intel/AMD-compatible CPUs; synonymous for `x86_64`, though `amd64` is the preferred form) - `ppc` (the `PowerPC` architecture popular in old Apple computers) - `armv7l` (little-endian 32bit ARM CPUs as found in the *Raspberry Pi 3*) - BIT is the size of Pd's numbers in bits (usually `32`; for double-precision it will be `64`) Note that the archive should contain a single directory at the top level with NAME the same as the externals package itself. For example a freeverb~ externals package would contain a directory "freeverb~" at the top level of the zipfile in which the externals live. The version string must be enclosed by square brackets (`[]`) and start with a `v`. The version string itself must not contain any brackets or parentheses. Strictly speaking, the version (with the enclosing brackets) is optional, however it is highly suggested that you provide it. The curly braces around the "(ARCH)" specifiers are only to indicate that this section can occur multiple times (or not at all). However, the round parentheses "()" enclosing the architectures string must be included to separate the architectures visibly from each other. In plain English this means: > the library-name, followed by an optional version string (starting with `[v` > and ending with `]`), followed by zero or more architecture specifications > (each surrounded by `(`parentheses`)`), and terminated by `.dek`. Here is the actual regular expression used: (.*/)?([^\[\]\(\)]+)(\[v[^\[\]\(\)]+\])?((\([^\[\]\(\)]+\))*)\.(dek) with the following matching groups: - ~~`#0` anything before the path (always empty and *ignored*)~~ - ~~`#1` = path to filename (*ignored*)~~ - `#2` = library name - `#3` = options (including the version) - `#4` = archs - ~~`#5` = last arch in archs (*ignored*)~~ - `#6` = extension ('dek') Some examples: adaptive[v0.0.extended](Linux-i386-32)(Linux-amd64-32).dek adaptive[v0.0.extended](Sources).dek freeverb~(Darwin-i386-32)(Darwin-x86_64-32)(Sources).dek list-abs[v0.1].dek ## Sourceful uploads `deken` is very much about *sharing*. To make sharing a more lasting experience, `deken` encourages the upload of "source-packages" besides (pre-compiled) binary packages. This is especially important if you are uploading a library that has been released under a license that requires you to share sources along with binaries (e.g. software licensed under the Gnu GPL), where it is your obligation to provide the source code to the end users. In other situations, having Source packages might be less important (e.g. it is fine to use `deken` with closed source libraries), however we would like to encourage sharing of sources. The way `deken` implements all this is by using a special pseudo architecture "Sources", which contains the sources of a library. `deken package` tries to automatically detect whether a package contains Sources by looking for common source code files (*.c, *.cpp, ...). When uploading a package, `deken` will ensure that you are *also* uploading a Source package of any library. If a Source package is missing, `deken` will abort operation. You can override this (e.g. because you have already uploaded a Source package; or because you simply do not want to upload any sources) by using the `--no-source-error` flag. For uploading a Source package along with binary packages, you can upload one package file with multiple archs (including a "Sources" arch) or multiple package files (one for the "Sources" arch). ~~~sh $ deken upload frobnozzel(Windows-i386-32)(Sources).dek $ deken upload foobar[v0.1](Linux-x86_64-32).dek foobar[v0.1](Sources).dek ~~~ ## objectlists Sometimes the user only knows the object they need, not the library. Therefore, a search initiated via the `deken-plugin` (Pd's package manager) also searches for *objects*. For this to work, the infrastructure must know which objects are contained in a library; this is done via an objectlist file. The objectlist file consists of exactly one line per object, with the object-name at the beginning, followed by a TAB (`\t`) and a short (single-line) description of the object. ~~~ frobnofy frobfurcate a bugle of numbers frobnofy~ signal frobfurcation ~~~ The objectlist file has the same name as the package with a `.txt` appended. E.g. if your library is called `frobnozzel(Windows-i386-32)(Sources).dek`, the objectlist file would have the name `frobnozzel(Windows-i386-32)(Sources).dek.txt` This file must be uploaded to the same directory as the `.dek` file. `deken` will try to automatically generate an objectlist file for a package. It looks for all "*-help.pd" files in the library directory, and creates an entry in the objectlists for each. The short description is taken from the `DESCRIPTION` comment in the `[pd META]` subpatch within the help-patch. If no DESCRIPTION comment can be found, a generic description is used. You can provide your own (manually maintained) objectlist file via the `--objects` flag: ~~~sh $ deken package --objects mylist.txt my_external ~~~ To prevent the creation/use of an objectlist file, pass an empty string ~~~sh $ deken package --objects "" my_external ~~~ In general, it is preferable if the description of the object in the META subpatch is included in the object's help file (and let deken generate the objectlist from it), as this allows others (humans, Pd, plugins, ...) to access the same information as well. ## Troubleshooting see [DEVELOPMENT.md](DEVELOPMENT.md) for some troubleshooting advice. ���������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������deken-0.10.4/developer/config.md��������������������������������������������������������������������0000664�0000000�0000000�00000000707�14764245604�0016456�0����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������Deken's config file lives in `~/.deken/config`. Here are the possible values: * `username` = Username on puredata.info for uploads. * `password` = Password on puredata.info for uploads. * `gpg_home` = Path to treat as GPG's homedir - defaults to GPG's default. * `key_id` = The key ID to use when signing binary packages - defaults to your secret key. * `gpg_agent` = Tell deken to invoke GPG with the `--use-agent` flag. All values are optional. ���������������������������������������������������������deken-0.10.4/developer/coverage.sh������������������������������������������������������������������0000775�0000000�0000000�00000007176�14764245604�0017030�0����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������#!/bin/sh deken_username=${DEKEN_USERNAME:-${USER}} unset DEKEN_USERNAME VE=$(mktemp -d) VDATA="${VE}/data" mkdir -p "${VDATA}" teardown() { test -d "${VE}" && rm -rf "${VE}" exit $1 } makedata() { # get some externals curl -fsSL -o "${VDATA}/deken-test.zip" "https://puredata.info/Members/zmoelnig/tests/deken-test.zip" || teardown unzip -d "${VDATA}/" "${VDATA}/deken-test.zip" touch "${VDATA}/bla.dek" touch "${VDATA}/bla[v000].dek" touch "${VDATA}/nosource(Linux-amd64-32).dek" touch "${VDATA}/bla[v000](Linux-amd64-32).dek" touch "${VDATA}/options[flub].dek" touch "${VDATA}/foo-externals.zip" touch "${VDATA}/foo--externals.zip" touch "${VDATA}/foo-externals.tar.gz" touch "${VDATA}/foo--externals.tgz" touch "${VDATA}/foo[v1[2]].dek" touch "${VDATA}/foo.zip" touch "${VDATA}/frobnozzel.ko" mkdir "${VDATA}/empty.dir/" } runtests() { ${PY} ${PY} -h DEKEN_VERSION=1.2.3 ${PY} --version # test all sub-commands ${PY} update ${PY} upgrade ${PY} install ${PY} package ${PY} upload # test packaging ${PY} package "${VDATA}/deken-test" ${PY} package --version 000 "${VDATA}/deken-test" ${PY} package -v 000 --dekformat 1 "${VDATA}/deken-test" ${PY} package -v 000 --dekformat 3 "${VDATA}/deken-test" ${PY} package -v 000 --dekformat bla "${VDATA}/deken-test" for f in "${VDATA}"/*.*; do ${PY} package --version 000 "${f}" done # test uploading ${PY} upload deken-test*.dek DEKEN_USERNAME=${deken_username} ${PY} upload --version 000 "${VDATA}/deken-test" ${PY} upload "${VDATA}/bla.dek" ${PY} upload --no-source-error "${VDATA}"/nosource*.dek ${PY} upload --ask-password "${VDATA}/bla.dek" ${PY} upload --destination https://example.com/%u "${VDATA}/bla.dek" DEKEN_USERNAME=${deken_username} ${PY} upload --destination /Members/${deken_username}/software/tmp/ "${VDATA}/bla.dek" ${PY} upload "${VDATA}/options[flub].dek" ${PY} upload "${VDATA}/frobnozzel.ko" ${PY} upload --dekformat 1 --version 000 "${VDATA}/empty.dir" } covconf() { cat <<EOF [run] plugins = hy_coverage_plugin EOF } fixeasywebdav() { cat >"${VE}/fix_easywebdav.py" <<EOF def easywebdav2_patch1(): try: import os.path import easywebdav2 print("trying to fix 'easywebdav2'") A=""" for dir_ in dirs:\n try:\n self.mkdir(dir, safe=True, **kwargs)""" B=""" for dir_ in dirs:\n try:\n self.mkdir(dir_, safe=True, **kwargs)""" filename = os.path.join(os.path.dirname(easywebdav2.__file__), 'client.py') print(filename) with open(filename, "r") as f: data = f.read() data = data.replace(A, B) with open(filename, "w") as f: f.write(data) except Exception as e: print("FAILED to patch 'easywebdav2', continuing anyhow...\n %s" % (e)) easywebdav2_patch1() EOF python "${VE}/fix_easywebdav.py" } virtualenv -p python3 "${VE}" || teardown $? . "${VE}/bin/activate" pip install -r requirements.txt pip install coverage PY="$(which python) pydeken.py" COVERAGE=$(which coverage) if [ "x${COVERAGE}" != "x" ]; then pip install hy-coverage-plugin covconf > "${VE}/coveragerc" "${COVERAGE}" erase PY="${COVERAGE} run --rcfile ${VE}/coveragerc -a --include deken*.hy pydeken.py -vvv" fi echo "PY: $PY" fixeasywebdav makedata runtests 2>&1 | tee coverage.log if [ "x${COVERAGE}" != "x" ]; then "${COVERAGE}" report --rcfile ${VE}/coveragerc "${COVERAGE}" html --rcfile ${VE}/coveragerc -d coverage fi teardown ��������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������deken-0.10.4/developer/deken������������������������������������������������������������������������0000775�0000000�0000000�00000024073�14764245604�0015705�0����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������#!/usr/bin/env sh # Ensure this file is executable via `chmod a+x deken`, then place it # somewhere on your ${PATH}, like ~/bin. The rest of Deken will self-install # on first run into the ~/.deken/ directory. # Much of this code is pilfered from Clojure's Leiningen tool script="$0" script_args=$* ############################################################################## # variable declarations ############################################################################## export DEKEN_VERSION="0.10.4" : "${DEKEN_HOME:=${HOME}/.deken}" : "${DEKEN_GIT_BRANCH:=main}" : "${DEKEN_BASE_URL:=https://raw.githubusercontent.com/pure-data/deken/${DEKEN_GIT_BRANCH}/developer}" # allow the user to override the default python : "${PYTHON_BIN:=python3}" export DEKEN_HOME VIRTUALENV_URL="https://bootstrap.pypa.io/virtualenv/@pyversion@/virtualenv.pyz" case "$(uname -s)" in CYGWIN*) DEKEN_HOME=$(cygpath -w "${DEKEN_HOME}") ;; *) ;; esac # This needs to be defined before we call HTTP_CLIENT below if [ "${HTTP_CLIENT}" = "" ]; then if which curl >/dev/null; then if [ -n "${https_proxy}" ]; then CURL_PROXY="-x ${https_proxy}" fi HTTP_CLIENT="curl ${CURL_PROXY} -f -L -o" else HTTP_CLIENT="wget -O" fi fi if test -z "${systeminstalled}"; then # if this script resides in /usr/ and there's a deken.hy relative to it in the # share/ folder, we consider it system-installed if test -z "${0##/usr/*}" && test -e "${0%/*}/../share/deken/deken.hy"; then systeminstalled=true else systeminstalled=false fi fi if ${systeminstalled}; then DEKEN_HOME="${0%/*}/../share/deken" fi : "${DEKEN_HY:=${DEKEN_HOME}/deken.hy}" : "${DEKENHY:=${DEKEN_HOME}/virtualenv/bin/hy}" : "${SYSTEMHY:=$(command -v hy3 hy | head -1)}" : "${SYSTEMHY:=$(which hy3 hy 2>/dev/null| head -1)}" ############################################################################## # helper functions ############################################################################## error() { echo "$@" 1>&2 } countdown() { _countdown_t=$1 _countdown_t=$((_countdown_t)) while [ $_countdown_t -gt 0 ]; do printf "\r%2d" $((_countdown_t)) sleep 1 _countdown_t=$((_countdown_t-1)) done printf "\r \r" unset _countdown_t } fetch_file() { _fetch_file_dst=${1#file://} _fetch_file_src=${2#file://} if test -e "${_fetch_file_src}"; then cp -v "${_fetch_file_src}" "${_fetch_file_dst}" else ${HTTP_CLIENT} "${_fetch_file_dst}" "${_fetch_file_src}" fi unset _fetch_file_dst unset _fetch_file_src } uninstall_deken() { if ${systeminstalled}; then # on Debian we disallow uninstalling error "Uninstalling is disabled for system-provided deken!" error "Instead, use your package manager to remove deken." exit 1 fi error "I'm going to uninstall myself and my dependencies from ${DEKEN_HOME} now." error "Feel free to Ctrl-C now if you don't want to do this." countdown 5 error "Uninstalling deken." rm -rf "${DEKEN_HOME}" "$0" exit 0 } bail_install() { error "Self-installation of Deken failed." error "Please paste any errors in the bug tracker at https://github.com/pure-data/deken/issues" # remove all traces of our attempts to install. rm -rf "${DEKEN_HOME}" # bail from this script. exit 1 } bail_install_msg() { error "$@" bail_install } tell_reinstall() { cat <<EOF You can run 'deken install --self' or 'deken upgrade --self' anytime to re-install (or upgrade) your Deken installation. EOF } bail_requirements() { rm -f "${DEKEN_HOME}/requirements.txt" cat >/dev/stderr <<EOF Installation of requirements failed. You probably should install the following packages first: - 'python3-dev' - 'libffi-dev' - 'libssl-dev' EOF tell_reinstall >/dev/stderr exit 1 } bail_upgrade() { if ${systeminstalled}; then # on Debian we don't want the user to run upgrades themselves return fi cat >/dev/stderr <<EOF It seems your version of deken is out of sync. ($script has version ${DEKEN_VERSION}, but your installation is $1) EOF tell_reinstall >/dev/stderr echo >/dev/stderr } install_virtualenv() { if which virtualenv >/dev/null; then virtualenv --system-site-packages "${DEKEN_HOME}/virtualenv" else _install_virtualenv_pyversion=$("${PYTHON_BIN}" -c "import sys; print('%s.%s' % (sys.version_info.major, sys.version_info.minor))") _install_virtualenv_url=$(echo "${VIRTUALENV_URL}" | sed -e "s|@pyversion@|${_install_virtualenv_pyversion}|g") echo "Downloading & installing Virtualenv for ${_install_virtualenv_pyversion} using ${_install_virtualenv_url}" rm -rf "${DEKEN_HOME}/virtualenv.pyz" mkdir -p "${DEKEN_HOME}" fetch_file "${DEKEN_HOME}/virtualenv.pyz" "${_install_virtualenv_url}" && \ "${PYTHON_BIN}" "${DEKEN_HOME}/virtualenv.pyz" "${DEKEN_HOME}/virtualenv" rm -rf "${DEKEN_HOME}/virtualenv.pyz" unset _install_virtualenv_pyversion unset _install_virtualenv_url fi [ -d "${DEKEN_HOME}/virtualenv" ] || \ bail_install } install_deken() { if ${systeminstalled}; then # on Debian, we can skip installation return fi which "${PYTHON_BIN}" >/dev/null || \ bail_install_msg "Oops, no Python found! You need Python3 to run Deken: ${PYTHON_BIN} You can specify an alternative Python interpreter via the PYTHON_BIN envvar" error "This is your first time running deken on this machine." error "I'm going to install myself and my dependencies into ${DEKEN_HOME} now." error "Feel free to Ctrl-C now if you don't want to do this." countdown 3 error "Installing deken." mkdir -p "${DEKEN_HOME}" [ -e "${DEKEN_HOME}/requirements.txt" ] || (\ ( echo "Fetching Python requirements file: ${DEKEN_BASE_URL}/requirements.txt" && \ fetch_file "${DEKEN_HOME}/requirements.txt" "${DEKEN_BASE_URL}/requirements.txt" ) || bail_install) [ -e "${DEKEN_HOME}/requirements.txt" ] || bail_install [ -e "${DEKEN_HY}" ] || (\ ( echo "Fetching main hylang file: ${DEKEN_BASE_URL}/deken.hy" && \ fetch_file "${DEKEN_HY}" "${DEKEN_BASE_URL}/deken.hy" ) || bail_install) [ -e "${DEKEN_HY}" ] || bail_install [ -d "${DEKEN_HOME}/virtualenv" ] || install_virtualenv if ! [ -x "${DEKENHY}" ]; then echo "Installing deken library dependencies." "${DEKEN_HOME}/virtualenv/bin/pip" install -r "${DEKEN_HOME}/requirements.txt" || bail_requirements fi echo "${DEKEN_VERSION}" > "${DEKEN_HOME}/version.txt" } upgrade_deken() { if ${systeminstalled}; then # on Debian we disallow upgrading error "Direct upgrading is disabled for system-provided deken!" error "Instead, use your package manager to install newer versions." exit 1 fi # first upgrade this script itself echo "Upgrading ${script}." fetch_file "${DEKEN_HOME}/.upgrade-deken" "${DEKEN_BASE_URL}/deken" || ( error "Error upgrading deken"; exit 1; ) if diff -q "${DEKEN_HOME}/.upgrade-deken" "$script" >/dev/null; then # launcher script is already up-to-date rm "${DEKEN_HOME}/.upgrade-deken" else if test -w "${script}"; then cat "${DEKEN_HOME}/.upgrade-deken" > "${script}" rm "${DEKEN_HOME}/.upgrade-deken" error "The deken-installer has changed." error "Please re-run the last command" error "" error "Hint: ${script} ${script_args}" exit else rm "${DEKEN_HOME}/.upgrade-deken" error "Unable to update '${script}', proceeding anyhow..." fi fi # next upgrade our dependencies for f in requirements.txt deken.hy do echo "Fetching ${f} file: ${DEKEN_BASE_URL}/${f}" fetch_file "${DEKEN_HOME}/.upgrade-${f}" "${DEKEN_BASE_URL}/${f}" || ( error "Error upgrading ${f}"; exit 1; ) mv "${DEKEN_HOME}/.upgrade-${f}" "${DEKEN_HOME}/${f}" done # finally update the python dependencies "${DEKEN_HOME}/virtualenv/bin/pip" install -r "${DEKEN_HOME}/requirements.txt" || bail_requirements echo "${DEKEN_VERSION}" > "${DEKEN_HOME}/version.txt" echo "Successfully upgraded." } tryrun_deken() { if ${systeminstalled}; then "${SYSTEMHY}" "${DEKEN_HY}" "$@" exit $? fi # check if the 'deken' script and the actual implementation match _tryrun_deken_version=$(cat "${DEKEN_HOME}/version.txt" 2>/dev/null) if test -n "${_tryrun_deken_version}" && test "x${_tryrun_deken_version}" != "x${DEKEN_VERSION}"; then bail_upgrade "${_tryrun_deken_version}" fi unset _tryrun_deken_version if [ ! -x "${DEKENHY}" ]; then error "Unable to find '${DEKENHY}'" error "Try running '$0 install --self' or '$0 upgrade --self'" exit 1 fi if [ ! -e "${DEKEN_HY}" ]; then error "Unable to find '${DEKEN_HY}'" error "Try running '$0 install --self' or '$0 upgrade --self'" exit 1 fi "${DEKENHY}" "${DEKEN_HY}" "$@" } ############################################################################## # here starts the code ############################################################################## # catch 'uninstall --self' early, so we don't run into the "installed" checks if [ $# -eq 2 ] && [ "$1" = "uninstall" ] && [ "$2" = "--self" ]; then uninstall_deken exit fi if [ $# -eq 1 ] && [ "$1" = "--version" ]; then echo "${DEKEN_VERSION}" exit fi if [ "$(id -u)" -eq 0 ] && [ "${DEKEN_ROOT}" = "" ]; then error "WARNING: You're currently running as root; probably by accident." error "Press Control-C to abort or Enter to continue as root." error "Set DEKEN_ROOT=yes to disable this warning." read -r _ fi # make sure we are deployed [ -d "${DEKEN_HOME}" ] || install_deken # last check to make sure we can bootstrap [ -d "${DEKEN_HOME}" ] || bail_install # catch the special "upgrade" command if [ $# -eq 2 ] && [ "$2" = "--self" ]; then case "$1" in install) install_deken exit ;; update|upgrade) upgrade_deken exit ;; esac fi # run the real deken command with args passed through tryrun_deken "$@" ���������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������deken-0.10.4/developer/deken.hy���������������������������������������������������������������������0000664�0000000�0000000�00000324343�14764245604�0016324�0����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������#!/usr/bin/env hy ;; deken upload --version 0.1 ./freeverb~/ ;; This software is copyrighted by Chris McCormick, IOhannes m zmölnig and ;; others. ;; The following terms (the "Standard Improved BSD License") apply to all ;; files associated with the software unless explicitly disclaimed in ;; individual files: ;; ;; Redistribution and use in source and binary forms, with or without ;; modification, are permitted provided that the following conditions are ;; met: ;; ;; 1. Redistributions of source code must retain the above copyright ;; notice, this list of conditions and the following disclaimer. ;; 2. Redistributions in binary form must reproduce the above ;; copyright notice, this list of conditions and the following ;; disclaimer in the documentation and/or other materials provided ;; with the distribution. ;; 3. The name of the author may not be used to endorse or promote ;; products derived from this software without specific prior ;; written permission. ;; ;; THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY ;; EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, ;; THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A ;; PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR ;; BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, ;; EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED ;; TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, ;; DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ;; ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT ;; LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING ;; IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF ;; THE POSSIBILITY OF SUCH DAMAGE. (import hy.pyops [>= =]) (import sys) (import os) (import re) (import argparse) (import datetime) (import logging) (import configparser [ConfigParser]) (import io [StringIO]) (import urllib.parse [urlparse urlunparse]) (import itertools [chain]) (setv flatten chain.from_iterable) ;; setup logging (setv log (logging.getLogger "deken")) (log.addHandler (logging.StreamHandler)) ;; do nothing (defn nop [#* ignored_args] """do nothing; return None""" None) ;; simple debugging helper: prints an object and returns it (defn debug [x #* more] """print the argument and return it""" (if more (log.debug (+ #(x) more)) (log.debug x)) x) (defn log_exception []) (defn log_error [msg] (log.error msg) (log_exception)) (defn log_warning [msg] (log.warning msg) (log_exception)) (defn log_debug [msg] (log.debug msg) (log_exception)) ;; print a fatal error and exit with an error code (defn fatal [x [exit 1]] """print argument as an error message and exit""" (log.fatal x) (log_exception) (when (not (is exit None)) (sys.exit exit))) (defn filter-none [iterable] """filter all None-elements from the iterable""" (filter (fn [item] (not (is item None))) iterable)) (setv deken-home (os.path.expanduser (os.path.join "~" ".deken"))) (setv config-file-path (os.path.abspath (os.path.join deken-home "config"))) (setv version (or (.get os.environ "DEKEN_VERSION" None) (when (os.path.exists (os.path.join (os.path.dirname (os.path.dirname (os.path.abspath __file__))) ".git")) (try (do (import subprocess) (.strip (.decode (subprocess.check_output ["git" "describe" "--always"])))) (except [e Exception] None))) "<unknown.version>")) (setv default-destination (urlparse "https://puredata.info/Members/%u/software/%p/%v")) (setv default-searchurl "https://deken.puredata.info/search") (setv default-installpath (os.path.expandvars (os.path.expanduser (cond (= sys.platform "darwin") "~/Library/Pd" (= sys.platform "win32") "%AppData%/Pd" True "~/.local/lib/pd/extra/")))) (setv architecture-substitutes { "x86_64" ["amd64"] "amd64" ["x86_64"] "i686" ["i586" "i386"] "i586" ["i386"] "armv6l" [ "armv6KZ" "armv6" "arm"] "armv7l" [ "armv7" "armv6l" "armv6KZ" "armv6" "arm"] "PowerPC" [ "ppc"] "ppc" [ "PowerPC"] }) (setv normalized-architectures { "x86_64" "amd64" ;"i686" "i386" ;"i586" "i386" ;"i486" "i386" "armv6l" "armv6" ;"armv6KZ" "armv6" ;; arm6 + multiproKessing + Zecurity "armv7l" "armv7" ;"arm" "armv7" ;; all uploads using 'arm' seem to be armv7 "armv8l" "armv8" "PowerPC" "ppc" }) (setv default-floatsize None) (setv description_pattern (re.compile "^#X text -?[0-9]+ -?[0-9]+ DESCRIPTION (.*)")) (setv version_pattern (re.compile "^#X text -?[0-9]+ -?[0-9]+ VERSION (.*)")) ;; check whether a form executes or not (eval-when-compile (defmacro runs? [exp] `(try (do ~exp True) (except [] False)))) (defn binary-file? [filename] """check if <filename> contains binary data""" ;; files that contain '\0' are considered binary ;; UTF-16 can contain '\0'; but for our purposes, its a binary format :-) (defn contains? [f [needle "\0"]] (setv data (f.read 1024)) (cond (not data) False (in needle data) True True (contains? f needle))) (if (os.path.isdir filename) False (try (with [f (open filename "rb")] (contains? f (str-to-bytes "\0"))) (except [e Exception] (log_debug e))))) (defn stringify-tuple [t] (if t (tuple (lfor x t (if (is x None) "" (.lower (str x))))) (tuple))) (defn str-to-bytes [s] """convert a string into bytes""" (try (bytes s) (except [e TypeError] (bytes s "utf-8")))) (defn str-to-bool [s] """convert a string into a bool, based on its value""" (try (not (in (.lower s) ["false" "f" "no" "n" "0" "nil" "none"])) (except [e AttributeError] (not (not s))))) (defn byte-to-int [b] """convert a single byte (e.g. an element of bytes()) into an integer""" (try (ord b) (except [e TypeError] (int b)))) (defn join-nonempty [joiner elements] """join all non-empty (non-false) elements""" (.join joiner (lfor x elements :if x (str x)))) ;; concatenate dictionaries - hylang's assoc is broken (defn dict-merge [dict0 #* dicts] """merge several dictionaries; if a key exists in more than one dict, the latest takes precedence""" (defn dict-merge-aux [dict0 dicts] (for [d dicts] (when d (dict0.update d))) dict0) ;; we need the aux just to prevent side-effects on dict0 (dict-merge-aux (.copy dict0) dicts)) ;; apply attributes to objects in a functional way (defn set-attr [obj attr value] """set an attribute of an obj in a functional way; return the obj""" (setattr obj attr value) obj) ;; get multiple attributes as list (defn get-attrs [obj attributes [default None]] """return a list of values, one for each attr in <attributes>""" (lfor _ attributes (getattr obj _default))) ;; get multiple values from a dict (give keys as list, get values as list) (defn get-values [coll keys] """return a list of values from a dictionary, pass the keys as list""" (lfor _ keys (get coll _))) ;; get a value at an index/key or a default (defn try-get [elements index [default None]] """get a value at an index/key, falling back to a default""" (try (get elements index) (except [e TypeError] default) (except [e KeyError] default) (except [e IndexError] default))) (defn first [coll] """return first item from `coll`.""" (for [f coll] (return f))) ;; replace multiple words (given as pairs in <repls>) in a string <s> (defn replace-words [s repls] """replace multiple words (given as pairs in <repls>) in a string <s>""" ;; https://stackoverflow.com/a/6117124/1169096 ;; rep = dict((re.escape(k), v) for k, v in rep.iter()) ;; pattern = re.compile("|".join(rep.keys())) ;; text = pattern.sub(lambda m: rep[re.escape(m.group(0))], text) (setv repls (dfor #( k v) repls (re.escape k) v)) (.sub (re.compile (.join "|" repls)) (fn [m] (get repls (re.escape (.group m 0)))) s)) ;; execute a command inside a directory (defn in-dir [destination f #* args] """execute a command f(args) inside a directory""" (setv last-dir (os.getcwd)) (os.chdir destination) (setv result (f #* args)) (os.chdir last-dir) result) ;; TODO: refactor 'listdir' and 'get-files-from-dir' into a single function (defn fix-easywebdav2 [pkg [broken " for dir_ in dirs:\n try:\n self.mkdir(dir, safe=True, **kwargs)"] [fixed " for dir_ in dirs:\n try:\n self.mkdir(dir_, safe=True, **kwargs)"] [exit 1]] """try to patch easywebdav2, it's broken as of 1.3.0""" (try (do (setv filename (os.path.join (os.path.dirname pkg.__file__) "client.py")) (with [f (open filename "r")] (setv data (f.read))) (when (in broken data) (try (do (with [f (open filename "w")] (f.write (.replace data broken fixed))) ;; TODO: stop execution and require the user to re-start (setv msg ["Fixing a problem with the 'easywebdav2' module succeeded."]) (when (not (is exit None)) (.append msg "Please re-run your command!")) (fatal (.join "\n" msg) exit)) (except [e OSError] (do (log.error "The 'easywebdav2' module is broken, and trying to fix the problem failed,") (log.error "so I will not be able to create directories on the remote server.") (log.error "As a workaround, please manually create any required directory on the remote server.") (log.error "For more information see https://github.com/zabuldon/easywebdav/pull/1") (log_exception))) ))) (except [e Exception] (log_debug (% "Unable to patch 'easywebdav2'\n%s" e))))) ;; read in the config file if present (defn read-config [configstring [config-file (ConfigParser)]] """read the configuration into a dictionary""" (try (config-file.read_file (StringIO configstring)) (except [e AttributeError] (config-file.readfp (StringIO configstring)))) (dict (config-file.items "default"))) (setv config (read-config (+ "[default]\n" (try (.read (open config-file-path "r"))(except [e Exception] ""))))) ;; try to obtain a value from environment, then config file, then prompt user (defn get-config-value [name #* default] """try to get a value first from the envvars, then from the config-file and finally fall back to a default""" (first (filter (fn [x] (not (is x None))) [ ;; try to get the value from an environment variable (os.environ.get (+ "DEKEN_" (.replace (.upper name) "-" "_"))) ;; try to get the value from the config file (config.get name) ;; finally, try the default (first default)]))) ;; prompt for a particular config value for externals host upload (defn prompt-for-value [name [forstring ""]] """prompt the user for a particular config value (with an explanatory text)""" ((try raw_input (except [e NameError] input)) (% (+ "Environment variable DEKEN_%s is not set and the config file %s does not contain a '%s = ...' entry.\n" "To avoid this prompt in future please add a setting to the config or environment.\n" "Please enter %s %s:: ") #( (name.upper) config-file-path name name forstring)))) (defn askpass [[prompt "Password: "]] """prompt the user for a password""" (import getpass) (getpass.getpass prompt)) (defn merge-url [url fallback-url] """merge multiple URLs""" ;; replace (scheme, netloc) of url with fallback-url (if they are missing in url) ;; "", "https://pd.info/pizza/salami" -> "https://pd.info/pizza/salami" ;; "/foo/bar", "https://pd.info/pizza/salami" -> "https://pd.info/foo/bar" ;; "https://pd.info/", ... -> "https://pd.info/" ;; "https://pd.info/x/y", ... -> "https://pd.info/x/y" (if (any url) (if url.netloc ;; we can't check the scheme, as on windows it might be "C:" (if destination is a simple "/foo/bar") url (urlparse (urlunparse (+ (list (cut fallback-url 2)) (list (cut url 2 None)))))) ;; url has no scheme://netloc component fallback-url)) (defn print-system-info [args] """print information about the environment we are running in""" (print "============= DEKEN =============") (print "Version :" version) (print "Config :" config-file-path) (print "Install-path:" default-installpath) (print "LogLevel :" (logging.getLevelName (log.getEffectiveLevel))) (print "Platform :" (.join "-" (native-arch))) (print) (print "============= SYSTEM ============") (print "Script :" __file__) (print "Executable :" sys.executable) (print "Hy :" (do (import hy) hy.__version__)) (print "Python :" (.join "; " (.splitlines sys.version))) (print "PyPrefix :" sys.prefix) (print "PyBasePrefix:" (try sys.base_prefix (except [e Exception] None))) (print "System :" sys.platform) (try (print "Windows :" (sys.getwindowsversion)) (except [e Exception])) (print "PATH :" (.get os.environ "PATH")) True ) (defn package-uri? [URI] """naive check whether the given URI seems to be a deken-package""" (or (.endswith URI ".dek") (.endswith URI "-externals.zip") (.endswith URI "-externals.tgz") (.endswith URI "-externals.tar.gz"))) (defn --packages-from-args-- [packages requirement-files] """return a set of packages specified either directly (<packages>) or indirectly (<requirement-files>)""" (defn req2pkg [req] (try (with [f (open req "r")] (lfor line (.readlines f) (.strip line))) (except [e OSError] (fatal (% "Unable to open requirements-file '%s'" #( req)))))) (defn reqs2pkgs [reqs] (flatten (lfor f reqs (req2pkg f)))) (.union (set packages) (reqs2pkgs requirement-files))) (defn native-arch [] """guesstimate on the native architecture""" (defn amd64? [cpu] (if (= cpu "x86_64") "amd64" cpu)) (import platform) #( (platform.system) (amd64? (platform.machine)) "32")) (defn compatible-arch? [need-arch have-archs] """check whether <have-archs> contains an architecture that is compatible with <need-arch>""" (defn simple-compat [need have] (or (= need have) (= need "*"))) (defn cpu-compat [need have] ;; is <a> a subset of <b>? (or (simple-compat need have) (in have (or (try-get architecture-substitutes need) [])))) (defn compat? [need-arch have-arch] (try (or (= have-arch need-arch) (and ;; OS = OS (simple-compat (get need-arch 0) (get have-arch 0)) ;; CPU = CPU (cpu-compat (get need-arch 1) (get have-arch 1)) ;; floatsize = floatsize (simple-compat (get need-arch 2) (get have-arch 2)))) (except [e Exception] (log_error (% "incompatible archs: %s != %s" #( need-arch have-arch)))))) (defn compat-generator [need-arch have-archs] (setv na (stringify-tuple need-arch)) (for [ha have-archs] (yield (compat? na (stringify-tuple ha))))) (log.debug "compatible-arch? %s IN %s" need-arch have-archs) (cond (not have-archs) True ;; archs is 'all' which matches any architecture (= need-arch "*") True ;; we don't care True (any (compat-generator need-arch have-archs)))) (defn compatible-archs? [need-archs have-archs] """check whether <have-archs> contains *any* of the architectures listed in <need-archs>""" (defn compat-generator [need-archs have-archs] (for [na need-archs] (yield (compatible-arch? na have-archs)))) (any (compat-generator need-archs have-archs))) (defn sort-archs [archs] """alphabetically sort list of archs with 'Sources' always at the end""" (+ (sorted (.difference (set archs) (set ["Sources"]))) (if (in "Sources" archs) ["Sources"] []))) (defn split-archstring [archstring [fixdek0 False]] """split an single archstring like 'Linux-amd64-32' into an arch-tuple""" (setv t (.split archstring "-")) (when (and fixdek0 (> (len t) 2)) (setv (get t 2) "32")) (tuple t)) (defn split-archstrings [archstring [fixdek0 False]] """split an archstring like '(Linux-amd64-32)(Windows-i686-32)' into a list of arch-tuples""" ;; if fixdek0 is True, this forces the floatsize to "32" (if archstring (lfor x (re.findall r"\(([^()]*)\)" archstring) (split-archstring x fixdek0)) [])) (defn normalize-arch [arch] """normalize the <arch> tuple with generic CPUs""" (try (do (setv [os cpu floatsize] arch) #(os (try-get normalized-architectures cpu cpu) floatsize)) (except [e ValueError] arch))) (defn arch-to-string [arch] """convert an architecture-tuple into a string""" (.join "-" (stringify-tuple arch))) (defn --archs-default-floatsize-- [[filename None]] (defn doit [floatsize filename] (log.warning (if filename (% "'%s' has no relevant symbols!...assuming floatsize=%s" #( filename floatsize)) (% "No relevant symbols found!...assuming floatsize=%s" #( filename)))) floatsize) (if default-floatsize (doit default-floatsize filename) (log.error (+ "OUCH: " (% "Couldn't detect float-size%s" (if filename (% " for '%s'" filename) "")) "\n and no default set, assuming None" "\n use '--default-floatsize <N>' to override)")))) (defn --pack-architectures-- [archs] """remove duplicate architectures; TODO remove archs with floatsize=0 if any package has a floatsize!=0""" (defn --drop-floatsize0-archspecific-- [archs others] """drop entries with floatsize=0 if the same OS/cpu also has a floatsize!=0""" (for [#( os cpu floatsize) archs] (setv (get archdict #( os cpu)) (.union (try-get archdict #( os cpu) (set)) [floatsize]))) (.union (set (flatten (lfor #( #( os cpu) floatsizes) (.items archdict) (lfor fs (or (list (filter bool floatsizes)) [0]) #( os cpu fs))))) others)) (defn --drop-floatsize0-any-- [archs others] """drop entries with fs=0, if there is *any* OS/cpu with a fs!=0""" (.union (set (if (list (filter bool (set (lfor #(_ _ fs) archs fs)))) ;; true if archs has floatsizes other than 0 (lfor a archs :if (get a 2) a) ;; only archs with floatsize!=0 archs)) ;; only archs with floatsize==0 others)) (setv archs (set archs)) (setv others (sorted (lfor a archs :if (!= (len a) 3) (tuple a)) :key str)) ;; 'Sources' (setv archs (sorted (lfor a archs :if ( = (len a) 3) (tuple a)) :key str)) ;; OS/CPU/floatsize tuples (setv archdict {}) (--drop-floatsize0-any-- archs others)) ;; takes the externals architectures and turns them into a string) (defn get-architecture-string [folder [recurse-subdirs False] [extra-files []]] """get architecture-string for all Pd-binaries in the folder""" (defn _get_archs [archs] (if archs (+ "(" (.join ")(" (list (sort-archs archs))) ")") "")) (_get_archs (lfor arch (--pack-architectures-- (get-externals-architectures folder :extra-files extra-files :recurse-subdirs recurse-subdirs)) (.join "-" (lfor parts arch (str parts)))))) ;; check if a particular file has an extension in a set (defn test-extensions [filename extensions] """check if filename has one of the extensions in the set""" (any (lfor e extensions :if (.endswith (.lower filename) e) e))) ;; check for a particular file in a directory, recursively (defn test-filename-under-dir [pred dir] (any (map (fn [w] (any (map pred (get w 2)))) (os.walk dir)))) ;; check if a particular file has an extension in a directory, recursively (defn test-extensions-under-dir [dir extensions] (test-filename-under-dir (fn [filename] (test-extensions filename extensions)) dir)) ;; examine a folder for externals and return the architectures of those found (defn get-externals-architectures [folder [extra-files []] [recurse-subdirs False]] """examine a folder for external binaries (and sources) and return the architectures of those found""" (defn listdir [folder [recurse-subdirs True]] (if recurse-subdirs (lfor #( dirname subdirs filenames) (os.walk folder) f filenames (os.path.join dirname f)) (lfor f (os.listdir folder) (os.path.join folder f)))) (sum (+ (if (test-extensions-under-dir folder [".c" ".cpp" ".cxx" ".cc"]) [[#("Sources")]] []) (lfor f (+ (listdir folder recurse-subdirs) extra-files) :if (os.path.exists f) (get-external-architecture f))) [])) (defn get-external-architecture [filename] """get the architecture(s) of a single external since a single binary might hold multiple architectures, this returns a list of (OS, CPU, floatsize) tuples """ ;; new style extensions '\.(?P<os>[a-z]+)-(?P<cpu>[a-z0-9_]+)-(?P<floatsize>(32|64|0))\.(so|dll)' are a *strong* hint - complain otherwise ;; the legacy extensions ('\.pd_(?P<os>[a-z]+)', '\.(?P<os>[a-z])_(?P<cpu>[a-z0-9_]+)' can only be single-precision (or no-precision) - complain otherwise ;; the generic extensions '.so' and '.dll' are more tricky, as they might be helper-libraries ;; ;; we *might* want to complain if the filename says 'fat' on non-darwin (defn --guess-arch-from-dekextension-- [filename] (setv x (re.match r"(?:.*)\.(?P<os>[a-z]+)-(?P<cpu>[a-z0-9]+)-(?P<floatsize>(32|64|0))\.(so|dll)$" filename)) (when x #((.group x "os") (.group x "cpu") [(int (.group x "floatsize"))]))) (defn --guess-arch-from-pd_extension-- [filename] (setv x (re.match r"(?:.*)\.pd_(?P<os>[a-z]*)$" filename)) (when x #((.group x "os") None [32 0]))) (defn --guess-arch-from-shortextension-- [filename] (setv short-os { "m" "windows" "l" "linux" "d" "darwin" }) (setv x (re.match r"(?:.*)\.(?P<os>[dlm])_(?P<cpu>[a-z0-9]+)$" filename)) (when x #((get short-os (.group x "os")) (.group x "cpu") [32 0]))) (defn --get-archs-with-os-- [filename hint] (setv os (get hint 0)) (setv cpu (get hint 1)) (setv fs (get hint 2)) (when (= "fat" cpu) (do (when (!= "darwin" os) (log.error (% "'%s' suggests fat binary for unsupported os '%s'" #(filename os)))) (setv cpu None))) (setv archs (cond (= "windows" os) (get-windows-archs filename) (= "darwin" os) (get-mach-archs filename) True (get-elf-archs filename os))) (lfor a archs (do (setv OS (.lower (get a 0))) (setv CPU (.lower (get a 1))) (setv FS (get a 2)) (when (and os (!= OS os)) (log.error (% "'%s' suggests %s binary, but found %s" #(filename os OS)))) (when (and cpu (not (compatible-arch? cpu [CPU]))) (log.error (% "'%s' suggests %s binary, but found %s" #(filename cpu CPU)))) (when (and fs FS (not (in FS fs))) (log.error (% "'%s' suggests floatsize %r, but found %r" #(filename fs FS)))))) archs) (cond (--guess-arch-from-dekextension-- filename) (--get-archs-with-os-- filename (--guess-arch-from-dekextension-- filename)) (--guess-arch-from-pd_extension-- filename) (--get-archs-with-os-- filename (--guess-arch-from-pd_extension-- filename)) (--guess-arch-from-shortextension-- filename) (--get-archs-with-os-- filename (--guess-arch-from-shortextension-- filename)) (re.search r".*\.dll$" filename) (get-windows-archs filename) (re.search r".*\.dylib$" filename) (get-mach-archs filename) (re.search r".*\.so$" filename) (+ (get-elf-archs filename "Linux") (get-mach-archs filename)) True [] ) ;; (+ ;; (if (re.search r"\.(pd_linux|so|l_[^.]*)$" filename) (get-elf-archs filename "Linux") (list)) ;; (if (re.search r"\.(pd_freebsd|b_[^.]*)$" filename) (get-elf-archs filename "FreeBSD") (list)) ;; (if (re.search r"\.(pd_darwin|so|d_[^.]*|dylib)$" filename) (get-mach-archs filename) (list)) ;; (if (re.search r"\.(dll|m_[^.]*)$" filename) (get-windows-archs filename) (list)) ;; []) ) ;; class_new -> t_float=float; class_new64 -> t_float=double (defn --pdfunction-to-floatsize-- [function-name] """detect Pd-floatsize based on the list of <function-name> used in the binary""" (cond (in function-name ["_class_new" "class_new"]) 32 (in function-name ["_class_new64" "class_new64"]) 64 (in function-name ["_class_addmethod" "class_addmethod" "_sys_register_loader" "sys_register_loader"]) 0 True None)) ;; Linux ELF file (defn get-elf-archs [filename [oshint "Linux"]] """guess OS/CPU/floatsize for ELF binaries""" (setv elf-osabi { "ELFOSABI_SYSV" None "ELFOSABI_HPUX" "HPUX" "ELFOSABI_NETBSD" "NetBSD" "ELFOSABI_LINUX" "Linux" "ELFOSABI_HURD" "Hurd" "ELFOSABI_SOLARIS" "Solaris" "ELFOSABI_AIX" "AIX" "ELFOSABI_IRIX" "Irix" "ELFOSABI_FREEBSD" "FreeBSD" "ELFOSABI_TRU64" "Tru64" "ELFOSABI_MODESTO" "Modesto" ;; Novell Modesto "ELFOSABI_OPENBSD" "OpenBSD" "ELFOSABI_OPENVMS" "OpenVMS" "ELFOSABI_NSK" "NonStop" ;; NonStop Kernel "ELFOSABI_AROS" "AROS" ;; AROS Research Operating System (AmigaOS-like) "ELFOSABI_ARM_AEABI" None "ELFOSABI_ARM" None "ELFOSABI_STANDALONE" None}) (setv elf-cpu { ;; format: #( CPU elfsize littlendian) "id" #( "EM_386" 32 True) "i386" #( "EM_X86_64" 64 True) "amd64" #( "EM_X86_64" 32 True) "x32" #( "EM_ARM" 32 True) "arm" ;; needs more #( "EM_AARCH64" 64 True) "arm64" ;; more or less exotic archs #( "EM_IA_64" 64 False) "ia64" #( "EM_IA_64" 64 True) "ia64el" #( "EM_68K" 32 False) "m68k" #( "EM_PARISC" 32 False) "hppa" #( "EM_PPC" 32 False) "ppc" #( "EM_PPC64" 64 False) "ppc64" #( "EM_PPC64" 64 True) "ppc64el" #( "EM_S390" 32 False) "s390" ;; 31bit!? #( "EM_S390" 64 False) "s390x" #( "EM_SH" 32 True) "sh4" #( "EM_SPARC" 32 False) "sparc" #( "EM_SPARCV9" 64 False) "sparc64" #( "EM_ALPHA" 64 True) "alpha" ;; can also be big-endian #( 36902 64 True) "alpha" #( "EM_MIPS" 32 False) "mips" #( "EM_MIPS" 32 True) "mipsel" #( "EM_MIPS" 64 False) "mips64" #( "EM_MIPS" 64 True) "mips64el" ;; microcontrollers #( "EM_BLAFKIN" 32 True) "blackfin" ;; "Analog Devices Blackfin" #( "EM_BLACKFIN" 32 True) "blackfin" ;; "Analog Devices Blackfin" #( "EM_AVR" 32 True) "avr" ;; "Atmel AVR 8-bit microcontroller" e.g. arduino ;; dead archs ;; #( "EM_88K" 32 None) "m88k" ;; predecessor of PowerPC ;; #( "EM_M32" ) "WE32100" ;; Belmac32 the world's first 32bit processor! ;; #( "EM_S370" ) "s370" ;; terminated 1990 ;; #( "EM_MIPS_RS4_BE" ) "r4000" ;; "MIPS 4000 big-endian" ;; direct concurrent to the i486 ;; #( "EM_860" ) "i860" ;; terminated mid-90s ;; #( "EM_NONE", None, None) None ;; #( "RESERVED", None, None) "RESERVED" }) ;; values updated via https://sourceware.org/git/gitweb.cgi?p=binutils-gdb.git;a=blob;f=include/elf/arm.h;hb=HEAD#l93 (setv elf-armcpu [ "armPre4" "armv4" "armv4T" "armv5T" "armv5TE" "armv5TEJ" "armv6" "armv6KZ" "armv6T2" "armv6K" "armv7" "armv6_M" "armv6S_M" "armv7E_M" "armv8" "armv8R" "armv8M_BASE" "armv8M_MAIN" ]) (defn --get-elf-sysv-- [elffile] """try to guess the OS of a generic SysV elf file""" (or (when (.get_section_by_name elffile ".note.openbsd.ident") "OpenBSD") (when (.get_section_by_name elffile ".note.netbsd.ident") "NetBSD") None)) (defn do-get-elf-archs [elffile oshint] ;; get the size of t_float in the elffile (defn get-elf-floatsizes [elffile] (list (filter-none (lfor _ (.iter_symbols (elffile.get_section_by_name ".dynsym")) (--pdfunction-to-floatsize-- _.name))))) (defn get-elf-armcpu [cpu] (defn armcpu-from-aeabi [arm aeabi] (defn armcpu-from-aeabi-helper [data] (when data (get elf-armcpu (byte-to-int (get (get (.split (cut data 7 None) (str-to-bytes "\x00") 1) 1) 1))))) (armcpu-from-aeabi-helper (and (arm.startswith (str-to-bytes "A")) (arm.index aeabi) (.pop (arm.split aeabi))))) (or (when (= cpu "arm") (armcpu-from-aeabi (.data (elffile.get_section_by_name ".ARM.attributes")) (str-to-bytes "aeabi"))) cpu)) (lfor floatsize (or (get-elf-floatsizes elffile) [(--archs-default-floatsize-- filename)]) #( (or (elf-osabi.get elffile.header.e_ident.EI_OSABI) (when (= elffile.header.e_ident.EI_OSABI "ELFOSABI_SYSV") (--get-elf-sysv-- elffile)) oshint "Linux") (get-elf-armcpu (elf-cpu.get #( elffile.header.e_machine elffile.elfclass elffile.little_endian))) floatsize))) ;; un-lowercase the OS hint (setv oshint (or (first (lfor _ (.values elf-osabi) :if (= oshint (.lower (str _))) _)) oshint)) (try (do (import elftools.elf.elffile [ELFFile]) (do-get-elf-archs (ELFFile (open filename :mode "rb")) oshint)) (except [e Exception] (or (log_debug e) (list))))) ;; macOS MachO file (defn get-mach-archs [filename] """guess OS/CPU/floatsize for MachO binaries""" (setv macho-cpu { 1 "vac" 6 "m68k" 7 "i386" 16777223 "amd64" 8 "mips" 10 "m98k" 11 "hppa" 12 "arm" 16777228 "arm64" 13 "m88k" 14 "spark" 15 "i860" 16 "alpha" 18 "ppc" 16777234 "ppc64" }) (defn get-macho-arch [macho] (defn get-macho-floatsizes [header] (import macholib.SymbolTable [SymbolTable]) (list (filter-none (lfor #( _ name) (getattr (SymbolTable macho header) "undefsyms") (--pdfunction-to-floatsize-- (.decode name)))))) (defn get-macho-headerarchs [header] (lfor floatsize (or (get-macho-floatsizes header) [(--archs-default-floatsize-- filename)]) #( "Darwin" (macho-cpu.get header.header.cputype) floatsize))) (list (flatten (lfor hdr macho.headers (get-macho-headerarchs hdr))))) (try (do (import macholib.MachO [MachO]) (get-macho-arch (MachO filename))) (except [e Exception] (or (log_debug e) (list))))) ;; Windows PE file (defn get-windows-archs [filename] """guess OS/CPU/floatsize for PE (Windows) binaries""" (defn get-pe-floatsizes [cpu floatsizes] (if floatsizes (lfor floatsize floatsizes #( "Windows" cpu floatsize)) [#( "Windows" cpu (or (--archs-default-floatsize-- filename) (raise (Exception))))])) (defn get-pe-archs [pef cpudict] (pef.parse_data_directories) (get-pe-floatsizes (.lower (.pop (.split (cpudict.get pef.FILE_HEADER.Machine "") "_"))) (list (filter-none (lfor fun (flatten (lfor entry pef.DIRECTORY_ENTRY_IMPORT (lfor imp entry.imports (.decode imp.name)))) (--pdfunction-to-floatsize-- fun)))))) (try (do (import pefile) (get-pe-archs (pefile.PE filename :fast_load True) pefile.MACHINE_TYPE)) (except [e Exception] (list)))) (defn unhexmunge [filename] (defn --unhexmunge-- [filename hexlist] (if (= filename (.join "" (re.findall "0x[0-9a-fA-F][0-9a-fA-F]" filename))) (.join "" (lfor c hexlist (chr (int c 16)))) filename)) (--unhexmunge-- filename (re.findall "0x[0-9a-fA-F][0-9a-fA-F]" filename))) (defn get-description-from-helpfile [helpfile] """get a short-description of an object from the 'DESCRIPTION' section of it's help-patch (if it exists); as of now, the patch-file parser is a bit simplistic: - it tries to get rid of Pd's artificial line-breaks after 80 (or so) chars, but probably this makes problems with escaped linebreaks,... - it completely ignores any subpatches (so DESCRIPTION need not be in [pd META]) - it doesn't handle DESCRIPTIONs that span multiple 'text's if the file does not exist or doesn't contain a 'DESCRIPTION', this returns 'DEKEN GENERATED' """ (.replace (.replace (try-get (list (filter None (lfor _ (.split (try (with [f (open helpfile :errors "ignore")] (.read f)) (except [e OSError] "")) ";\n") (try-get (description_pattern.match (re.sub r"([^\\]), f [0-9]+$" r"\1" (.join " " (.splitlines _)))) 1 None)))) 0 "DEKEN GENERATED") "\t" " ") "\n" " ")) (defn get-version-from-metafile [metafile] """get the version of a library from the 'VERSION' section of it's meta-patch (if it exists); as of now, the patch-file parser is a bit simplistic: - it tries to get rid of Pd's artificial line-breaks after 80 (or so) chars, but probably this makes problems with escaped linebreaks,... - it completely ignores any subpatches (so VERSION need not be in [pd META]) - it doesn't handle VERSIONs that span multiple 'text's if the file does not exist or doesn't contain a 'VERSION', this returns an empty string """ (defn do-get-version [metafile] (.replace (.replace (try-get (list (filter None (lfor _ (.split (try (with [f (open metafile :errors "ignore")] (.read f)) (except [e OSError] "")) ";\n") (try-get (version_pattern.match (re.sub r"([^\\]), f [0-9]+$" r"\1" (.join " " (.splitlines _)))) 1 None)))) 0 "") "\t" " ") "\n" " ")) (setv version (do-get-version metafile)) (when version (log.warning (% "Extracted version from '%s' from %s" #(version metafile)))) (or version None)) (defn make-objects-file [dekfilename objfile [warn-exists True]] """generate object-list for <filename> from <objfile>""" ;; dekfilename exists: issue a warning, and don't overwrite it ;; objfile=='' don't create an objects-file ;; objfile==None generate from <objfile2> ;; objfile==zip-file extract from zip-file ;; objfile==TSV-file use directly (actually, we only check whether the file seems to not be binary) (defn get-files-from-zip [archive] (import zipfile) (try (.namelist (zipfile.ZipFile archive "r")) (except [e Exception] (log_debug e)))) (defn get-files-from-dir [directory [recursive False] [full_path False]] (if recursive (list (flatten (lfor #( root dirs files) (os.walk directory) (if full_path (lfor f files (os.path.join root f)) files)))) (try (lfor x (os.listdir directory) :if (os.path.isfile (os.path.join directory x)) (if full_path (os.path.join directory x) x)) (except [e OSError] [])))) (defn genobjs [input] (when input (lfor f input :if (f.endswith "-help.pd") (% "%s\t%s\n" #( (unhexmunge (cut (os.path.basename f) 0 -8)) (get-description-from-helpfile f)))))) (defn readobjs [input] (when (not (os.path.isdir input)) (try (with [f (open input)] (.readlines f)) (except [e Exception] (log_debug e))))) (defn writeobjs [output data] (when data (try (do (with [f (open output :mode "w")] (.write f (.join "" data))) output) (except [e Exception] (log_debug e))))) (defn do-make-objects-file [dekfilename objfilename] (cond (not dekfilename) None (not objfilename) None (= dekfilename objfilename) dekfilename True (writeobjs dekfilename (sorted (or (genobjs (if (os.path.isdir objfilename) (get-files-from-dir objfilename :full_path True) (get-files-from-zip objfilename))) (if (binary-file? objfilename) [] (readobjs objfilename)) []))))) (setv dekfilename (% "%s.txt" dekfilename)) (if (os.path.exists dekfilename) (do ;; already exists (log.info (% "Objects file '%s' already exists" dekfilename)) (when warn-exists (log.warning (% "WARNING: delete '%s' to re-generate objects file" dekfilename))) dekfilename) (do-make-objects-file dekfilename objfile))) ;; calculate the sha256 hash of a file (defn hash-file [file hashfn] """calculate the hash of a file""" (for [buf file] (hashfn.update buf)) (hashfn.hexdigest)) (defn hash-sum-file [filename [algorithm "sha256"] [blocksize -1]] """calculates the (sha256) hash of a file and stores it into a separate file""" (import hashlib) (defn do-hash-file [filename hashfilename hasher blocksize] (.write (open hashfilename :mode "w") (hash-file (open filename :mode "rb" :buffering blocksize) hasher)) hashfilename) (do-hash-file filename (% "%s.%s" #( filename algorithm)) (hashlib.new algorithm) blocksize)) (defn hash-verify-file [filename [hashfilename None] [blocksize -1] [algorithm None]] """verify that the hash of the <filename> file is the same as stored in the <hashfilename>""" (import hashlib) (defn filename2algo [filename] (cut (get (os.path.splitext filename) 1) 1 None)) (setv hashfilename (or hashfilename (+ filename ".sha256"))) (setv algorithm (or algorithm (filename2algo hashfilename))) (try (= (hash-file (open filename :mode "rb" :buffering blocksize) (hashlib.new algorithm)) (.strip (get (.split (.read (open hashfilename "r"))) 0))) (except [e #( OSError TypeError ValueError)] (log_exception)))) ;; handling GPG signatures (try (import gnupg) ;; read a value from the gpg config (except [e ImportError] (do (defn gpg-sign-file [filename] """sign a file with GPG (if the gnupg module is installed)""" (log.warning (+ (% "Unable to GPG sign '%s'\n" filename) "'gnupg' module not loaded"))) (defn gpg-verify-file [signedfile signaturefile] """verify a file with a detached GPG signature""" (log.warning (.join "\n" (list (% "Unable to GPG verify '%s'" #( signedfile)) "'gnupg' module not loaded"))) ))) (else (do (defn --gpg-unavail-error-- [state [ex None]] (log.warning (% "GPG %s failed:" state)) (when ex (log.warning ex)) (log.warning "Do you have 'gpg' installed?") (log.warning "- If you've received numerous errors during the initial installation,") (log.warning " you probably should install 'python-dev', 'libffi-dev' and 'libssl-dev'") (log.warning " and re-run `deken install`") (log.warning "- On OSX you might want to install the 'GPG Suite'") (log.warning "Signing your package with GPG is optional.") (log.warning " You can safely ignore this warning if you don't want to sign your package")) ;; generate a GPG signature for a particular file (defn gpg-verify-file [signedfile signaturefile] """verify a file with a detached GPG signature""" (defn get-gpg [] (setv gnupghome (get-config-value "gpg_home")) (setv gpg (try (set-attr (gnupg.GPG #** (if gnupghome {"gnupghome" gnupghome} {})) "decode_errors" "replace") (except [e OSError] (--gpg-unavail-error-- "init" e)))) (when gpg (setv gpg.encoding "utf-8")) gpg) (defn do-verify [data sigfile] (setv result (gpg.verify_data signaturefile data)) (when (not result) (log.debug result.stderr)) (bool result)) (setv gpg (get-gpg)) (when gpg (setv data (try (with [f (open signedfile "rb")] (f.read)) (except [e OSError] None))) (when (and data (os.path.exists signaturefile)) (do-verify data signaturefile)))) (defn gpg-sign-file [filename] """sign a file with GPG (if the gnupg module is installed)""" (defn gpg-get-config [gpg id] (try (get (lfor x (.readlines (open (os.path.expanduser (os.path.join (or gpg.gnupghome (os.path.join "~" ".gnupg")) "gpg.conf")))) :if (.startswith (.lstrip x) (.strip id) ) (get (.split (.strip x)) 1)) -1) (except [e [IOError IndexError]] None))) ;; get the GPG key for signing (defn gpg-get-key [gpg] (setv keyid (get-config-value "key_id" (gpg-get-config gpg "default-key"))) (try (first (lfor k (gpg.list_keys True) :if (cond keyid (.endswith (.upper (get k "keyid" )) (.upper keyid) ) True True) k)) (except [e IndexError] None))) (defn do-gpg-sign-file [filename signfile gnupghome use-agent] (log.info (% "Attempting to GPG sign '%s'" filename)) (setv gpg (try (set-attr (gnupg.GPG #** (dict-merge (if gnupghome {"gnupghome" gnupghome} {}) (if use-agent {"use_agent" True} {}))) "decode_errors" "replace") (except [e OSError] (--gpg-unavail-error-- "init" e)))) (when gpg (setv gpg.encoding "utf-8") (setv [keyid uid] (lfor _ ["keyid" "uids"] (try-get (gpg-get-key gpg) _ None))) (setv uid (try-get uid 0 None)) (setv passphrase (when (and (not use-agent) keyid) (print (% "You need a passphrase to unlock the secret key for\nuser: %s ID: %s\nin order to sign %s" #( uid keyid filename))) (askpass "Enter GPG passphrase: " ))) (setv signconfig (dict-merge {"detach" True} (if keyid {"keyid" keyid} {}) (if passphrase {"passphrase" passphrase} {}))) (when (and (not use-agent) (not passphrase)) (log.info "No passphrase and not using gpg-agent...trying to sign anyhow")) (try (do (setv sig (when gpg (gpg.sign_file (open filename "rb") #** signconfig))) (when (hasattr sig "stderr") (log.debug (try (str sig.stderr) (except [e UnicodeEncodeError] (.encode sig.stderr "utf-8"))))) (if (not sig) (log.warning "Could not GPG sign the package.") (do (with [f (open signfile "w")] (f.write (str sig))) signfile))) (except [e OSError] (--gpg-unavail-error-- "signing" e))))) ;; sign a file if it is not already signed (setv signfile (+ filename ".asc")) (setv gpghome (get-config-value "gpg_home")) (setv gpgagent (str-to-bool (get-config-value "gpg_agent"))) (if (os.path.exists signfile) (do (log.info (% "not GPG-signing already signed file '%s'" filename)) (log.info (% "delete '%s' to re-sign" signfile)) signfile) (do-gpg-sign-file filename signfile gpghome gpgagent)))))) (defn parse-requirement [spec] """parse a requirement-string """ ;; spec can be a library, or a library with version, e.g. "library==0.2.1" or "library>=1.2.3" ;; currently the only valid compatrators are: ">=", "==", "~=" ;; returns a tuple (library, version, comparator) (setv result (try (get-values (re.split "(.+)([~>=]=)(.+)" spec) [1 3 2]) (except [e IndexError] [spec None None]))) (setv (get result 2) (try-get {"~=" str.startswith "==" = ">=" >=} (get result 2) (fn [a b] True))) (tuple result)) (defn make-requirement-matcher [parsedspec] """create a boolean function to check whether a given package-dict matches a requirement""" ;; <parsedspec> is the output of (parse-requirement): a (library, version, comparator) tuple ;; currently the only valid compatrators are ">=" and "==" ;; returns a tuple (library, version, comparator) ;; (setv parsedspec (parse-requirement spec)) (setv package (get parsedspec 0)) (setv version (get parsedspec 1)) (setv compare (get parsedspec 2)) (fn [libdict] (try (and (= (get libdict "package") package) (compare (get libdict "version") version)) (except [e TypeError] None) (except [e KeyError] None)))) (defn make-requirements-matcher [specs] """create a boolean function to check whether a given package-dict matches any of the given requirements""" (if specs (fn [libdict] (any (lfor req-match (lfor spec specs :if spec (make-requirement-matcher spec)) (req-match libdict)))) (fn [libdict] True))) (defn sort-searchresults [libdicts [reverse False]] """sort <libdicts> (list of dictionaries)""" (sorted libdicts :reverse reverse :key (fn [d] #( (.lower (or (get d "package") "")) (.lower (or (get d "version") "")) (.lower (or (get d "timestamp") "")))))) (defn filter-older-versions [libdicts [depth 1]] """for each library (with a unique 'package' key) in <libdicts> leave only the latest <depth> versions""" (defn doit [libdicts depth] ;; create a dict with <package> keys, and the values being <libdict> lists (setv pkgdict {}) (for [lib libdicts] (do (setv pkgname (get lib "package")) (when (not (in pkgname pkgdict)) (setv (get pkgdict pkgname) [])) (setv l (get pkgdict pkgname)) (l.append lib))) ;; sort and truncate each dictvalue (setv result []) (for [key pkgdict] (setv result (+ result (cut (sorted (get pkgdict key) :reverse True :key (fn [d] #( (or (get d "version") "") (or (get d "timestamp") "")))) 0 depth)))) result) (if depth (doit libdicts depth) libdicts)) ;; zip up a single directory ;; http://stackoverflow.com/questions/1855095/how-to-create-a-zip-archive-of-a-directory (defn zip-file [filename] """create a ZIP-file with a default compression""" (import zipfile) (try (zipfile.ZipFile filename "w" :compression zipfile.ZIP_DEFLATED) (except [e RuntimeError] (zipfile.ZipFile filename "w")))) (defn zip-dir [directory-to-zip archive-file [extension ".zip"]] """create a ZIP-archive of a directory""" (setv zip-filename (+ archive-file extension)) (with [f (zip-file zip-filename)] (for [[root dirs files] (os.walk directory-to-zip)] (for [file-path (lfor file files (os.path.join root file))] (when (os.path.exists file-path) (f.write file-path (os.path.relpath file-path (os.path.join directory-to-zip ".."))))))) zip-filename) (defn unzip-file [archive-file [targetdir "."]] """extract all members of the zip archive into targetdir""" (try (do (import zipfile) (with [f (zipfile.ZipFile archive-file)] (f.extractall :path targetdir)) True) (except [e Exception] (or (log_debug (% "Unzipping '%s' failed" #( archive-file))) False)))) ;; tar up the directory (defn tar-dir [directory-to-tar archive-file [extension ".tar.gz"]] """create a (gzipped) TAR archive of a directory""" (import tarfile) (setv tar-file (+ archive-file extension)) (defn tarfilter [tarinfo] (setv tarinfo.name (os.path.relpath tarinfo.name (os.path.join directory-to-tar ".."))) tarinfo) (with [f (tarfile.open tar-file "w:gz")] (f.add directory-to-tar :filter tarfilter)) tar-file) (defn untar-file [archive-file [targetdir "."]] """extract all members of the tar archive into targetdir""" (try (do (import tarfile) (with [f (tarfile.open archive-file "r")] (f.extractall :path targetdir)) True) (except [e Exception] (or (log_debug (% "Untaring '%s' failed" #( archive-file))) False)))) ;; do we use zip or tar on this archive? (defn archive-extension [rootname] """default extension for dekformat.v0: if the architecture includes Windows, we use 'zip', else 'tar.gz'""" (if (or (in "(Windows" rootname) (not (in "(" rootname))) ".zip" ".tar.gz")) ;; v1: all archives are ZIP-files with .dek extension ;; v0: automatically pick the correct archiver - windows or "no arch" = zip (defn archive-dir [directory-to-archive rootname] """create an archive of a directory,using a method based on the extension""" ((cond (.endswith rootname ".dek") zip-dir (.endswith rootname ".zip") zip-dir True tar-dir) directory-to-archive rootname "")) ;; naive check, whether we have an archive: compare against known suffixes (defn archive? [filename] """(naive) check if the given filename is a (known) archive: just check the file extension""" (test-extensions filename [".dek" ".zip" ".tar.gz" ".tgz"])) ;; try to remove a file (but keep running if things go wrong) (defn try-remove-file [filename] """try to delete <filename>, but don't complain if things fail""" (when filename (try (os.remove filename) (except [e Exception] (log_debug e)))) None) ;; download a file (defn download-file [url [filename None] [output-dir "."] [warn-download-failure True]] """download a file from <url>, save it as <filename> (or a sane default); return the filename or None; make sure that no file gets overwritten""" (try (os.makedirs output-dir) (except [e Exception] (when (not (os.path.isdir output-dir)) (raise e)))) (defn unique-filename [filename] (defn unique-filename-number [filename number] (setv filename0 (% "%s.%s" #( filename number))) (if (not (os.path.exists filename0)) filename0 (unique-filename-number filename (+ number 1)))) (if (os.path.exists filename) (unique-filename-number filename 1) filename)) (defn save-data [outfile content] (try (do (with [f (open outfile "wb")] (.write f content)) outfile) (except [e OSError] (log_warning (% "Unable to download file: %s" #( e)))))) (import requests) (log.info (% "Downloading '%s' as %s" #( url filename))) (setv r (requests.get url)) (if (= 200 r.status_code) (save-data (unique-filename (os.path.join (or output-dir ".") (or filename (try (.strip (first (re.findall "filename=(.+)" (try-get r.headers "content-disposition" ""))) "\"") (except [e AttributeError] None)) (os.path.basename url) "downloaded_file"))) r.content) (when warn-download-failure (log.warning (% "Downloading '%s' failed with '%s'" #( url r.status_code)))))) ;; upload a zipped up package to puredata.info (defn upload-file [filepath destination username password] """upload a file to a destination via webdav, using username/password""" ;; get username and password from the environment, config, or user input (try (do (import easywebdav2) (fix-easywebdav2 easywebdav2) (setv easywebdav easywebdav2)) (except [e ImportError] (import easywebdav))) (defn do-upload-file [dav path filename] (log.info (% "Uploading '%s' to %s://%s%s" #( filename destination.scheme destination.hostname path))) (try (do ;; make sure all directories exist (dav.mkdirs path) ;; upload the package file (dav.upload filepath (+ path "/" filename))) (except [e easywebdav.client.OperationFailed] (fatal (+ (str e) "\n" (% "Couldn't upload to %s://%s%s!\n" #( destination.scheme destination.hostname path)) (% "Are you sure you have the correct username and password set for '%s'?\n" destination.hostname) (% "Please ensure the folder '%s' exists on the server and is writable." path)))))) (when filepath (setv filename (os.path.basename filepath)) (setv [pkg ver _ _] (parse-dekname filename)) (setv pkg (or pkg (fatal (% "'%s' is not a valid deken file(name)" filename)))) (setv ver (.strip (or ver "") "[]")) (setv path (str (replace-words (.rstrip destination.path "/") #( #( "%u" username) #( "%p" pkg) #( "%v" (or ver "")))))) (do-upload-file (easywebdav.connect destination.hostname #** {"username" username "password" password "protocol" destination.scheme}) path filename))) ;; upload an archive (given the archive-filename it will also upload some extra-files (sha256, gpg,...)) ;; returns a (username, password) tuple in case of success ;; in case of failure, this exits (defn upload-package [pkg destination username password] """upload a package (with sha256-file and gpg-signature if possible)""" (log.info (% "Uploading package '%s'" pkg)) (upload-file (hash-sum-file pkg) destination username password) (upload-file pkg destination username password) (upload-file (make-objects-file pkg None False) destination username password) (upload-file (gpg-sign-file pkg) destination username password) #( username password)) ;; upload a list of archives (with aux files) ;; returns a (username, password) tuple in case of success ;; in case of failure, this exits (defn upload-packages [pkgs destination username password skip-source] """upload multiple packages at once""" (when (not skip-source) (check-sources (sfor pkg pkgs (filename-to-namever pkg)) (sfor pkg pkgs (has-sources? pkg)) (when (= "puredata.info" (.lower (or destination.hostname default-destination.hostname))) username))) (for [pkg pkgs] (if (get (parse-dekname pkg) 0) (upload-package pkg destination username password) (log.warning (% "Skipping '%s', it is not a valid deken package" pkg)))) (log.warning "Your upload was successful.") (log.warning "Please note that it can take up to 24 hours before the package will appear") (log.warning "in deken-searches that allow others to download your package from the") (log.warning "Pd package repository.") #( username password)) ;; compute the archive filename for a particular external on this platform ;; v1: "<pkgname>[v<version>](<arch1>)(<arch2>).dek" ;; v0: "<pkgname>-v<version>-(<arch1>)(<arch2>)-externals.tar.gz" (resp. ".zip") (defn make-archive-name [folder pkgname version [filenameversion 1] [recurse-subdirs False] [extra-arch-files []] [output-dir "."]] """calculate the dekenfilename for a given folder (embedding version and architectures in the filename)""" (defn do-make-name [pkgname version archs filenameversion] (cond (= filenameversion 1) (+ pkgname (if version (% "[v%s]" version) "") archs ".dek") (= filenameversion 0) (+ pkgname (if version (% "-v%s-" version) "") archs "-externals" (archive-extension archs)) True (fatal (% "Unknown dekformat '%s'" filenameversion)))) (setv metafile (os.path.join folder (% "%s-meta.pd" pkgname))) (when (and (is None version) (os.path.exists metafile)) (setv version (or (get-version-from-metafile metafile) None))) (setv version (if (and version (.startswith version "v")) (cut version 1 None) version)) (do-make-name (os.path.normpath (os.path.join output-dir (or pkgname (os.path.basename folder)))) (cond (is version None) (fatal (+ (% "No version for '%s'!\n" folder) " Please provide the version-number via the '--version' flag.\n" (% " If '%s' doesn't have a proper version number,\n" folder) (% " consider using a date-based fake version (like '0~%s')\n or an empty version ('')." (.strftime (datetime.date.today) "%Y%m%d")))) version version) (get-architecture-string folder :recurse-subdirs recurse-subdirs :extra-files extra-arch-files) filenameversion)) ;; create additional files besides archive: hash-file and gpg-signature (defn archive-extra [dekfile [objects None]] """create additional files besides archive: hash-file and GPG-signature""" (log.info (% "Packaging %s" dekfile)) (hash-sum-file dekfile) (when objects (if (dekfile.endswith ".dek") (make-objects-file dekfile objects) (log.warning "Objects file generation is only enabled for dekformat>=1...skipping!"))) (gpg-sign-file dekfile) dekfile) ;; parses a filename into a (pkgname version archs extension) tuple ;; missing values are None (defn parse-dekname0 [filename] """parse a dekenformat.v0 filename into a (pkgname version archs extension) tuple""" (try (get-values ;; parse filename with a regex (re.split r"(.*/)?(.+?)(-v(.+)-)?((\([^\)]+\))+|-)*-externals\.([a-z.]*)" filename) ;; extract only the fields of interested [2 4 5 7]) (except [e IndexError] []))) (defn parse-dekname1 [filename] """parse a dekenformat.v1 filename into a (pkgname version archs extension) tuple""" (try (get-values (re.split r"(.*/)?([^\[\]\(\)]+)(\[v([^\[\]\(\)]+)\])?((\([^\[\]\(\)]+\))*)\.(dek(\.[a-z0-9_.-]*)?)" filename) [2 4 5 7]) (except [e IndexError] []))) (defn parse-dekname [filename] """parse a dekenformat filename (any version) into a (pkgname version archs extension) tuple""" (lfor x (or (parse-dekname1 filename) (parse-dekname0 filename) [None None None None]) (or x None))) (defn filename-to-namever [filename] """extract a <name>/<version> string from a filename""" (join-nonempty "/" (get-values (parse-dekname filename) [0 1]))) ;; check if the list of archs contains sources (or is arch-independent) (defn source-arch? [arch] """check if the arch string contains sources (or doesn't need them because its arch-independent)""" (or (not arch) (in "(Sources)" arch))) ;; check if a package contains sources (and returns name-version to be used in a SET of packages with sources) (defn has-sources? [filename] """return name/version if the filename contains sources (so we check whether we still need to upload sources)""" (when (source-arch? (try-get (parse-dekname filename) 2)) (filename-to-namever filename))) ;; check if the given package has a sources-arch on puredata.info (defn check-sources@puredata-info [pkg username] """check if there has been a sourceful upload for a given package""" (import requests) (log.info (% "Checking puredata.info for Source package for '%s'" pkg)) (in pkg ;; list of package/version matching 'pkg' that have 'Source' architecture (lfor p (lfor x (.splitlines (getattr (requests.get (% "http://deken.puredata.info/search?name=%s" (get (.split pkg "/") 0))) "text")) :if (= username (try-get (.split x "\t") 2)) (try-get (.split (try-get (.split x "\t") 1) "/") -1)) ;; filename part of the download URL (has-sources? p)))) ;; check if sources archs are present by comparing a SET of packagaes and a SET of packages-with-sources (defn check-sources [pkgs sources [puredata-info-user None]] """bail out if there are no sources on puredata.info yet and we don't currently upload sources""" (for [pkg pkgs] (when (and (not (in pkg sources)) (not (and puredata-info-user (check-sources@puredata-info pkg puredata-info-user)))) (fatal (+ (% "Missing sources for '%s'!\n" pkg) "(You can override this error with the '--no-source-error' flag,\n" " if you absolutely cannot provide the sources for this package)\n"))))) ;; get the password, either from ;; - a password agent ;; - the config-file (no, not really?) ;; - user-input ;; if force-ask is set, skip the agent ;; store the password in the password agent (for later use) (defn get-upload-password [username force-ask] """get password from keyring agent, config-file (ouch), or user-input""" (or (when (not force-ask) (or (try (do (import keyring) (keyring.get_password "deken" username)) (except [e RuntimeError] (log_debug e)) (except [e Exception] (log_warning e))) (get-config-value "password"))) (askpass (% "Please enter password for uploading as '%s': " username)))) (defn user-agent [] """get the user-agent string of this application""" ;; "Deken/${::deken::version} ([::deken::platform2string]) ${pdversion} Tcl/[info patchlevel]" (% "Deken/%s (%s) Python/%s" #( version (.join "-" (native-arch)) (get (.split sys.version) 0)))) (defn categorize-search-terms [terms [libraries True] [objects True]] """split the <terms> into objects, libraries and versioned-libraries; returns a dict""" ;; versioned requirements (e.g. 'foo>=3.14') are always libraries, and go into 'libraries' (without the version) and 'versioned-libraries' (as tuples) ;; unversioned terms will show up in 'libraries' and/or 'objects', depending on which flag is True ;; a term that looks like an URL, will appear (only) in 'urls' (setv libs (set)) (setv objs (set)) (setv vlibs (set)) (setv urls (set)) (for [t terms] (do (setv vlib (parse-requirement t)) (if (getattr (urlparse t) "scheme") (urls.add t) (do (when libraries (if (get vlib 1) (do (vlibs.add vlib) (libs.add (get vlib 0))) (libs.add t))) (when objects (if (get vlib 1) None (objs.add t))))))) {"libraries" (sorted libs) "objects" (sorted objs) "versioned-libraries" (sorted vlibs) "urls" (sorted urls)} ) (defn search [searchurl libraries objects] """search needle in libraries (if True) and objects (if True)""" (defn parse-tab-separated-values [data] (defn parse-tsv [description [URL None] [uploader None] [date None] #* args] (setv result (dict-merge {"description" description "URL" URL "uploader" uploader "timestamp" date} (dict (zip ["package" "version" "architectures" "extension"] (parse-dekname (or URL "")))))) (setv (get result "architectures") (lfor a (split-archstrings (get result "architectures") (not (.endswith URL ".dek"))) (normalize-arch a))) result) (lfor line (.splitlines (getattr r "text")) :if line (parse-tsv #* (.split line "\t")))) (defn parse-json-results [data] ;; (setv d {"query" "bla bla" ;; "results" {"foo" "bar" ;; "libraries" { ;; "zexy" {"1.2.3" {"library" "zexy" "author" "zmoelnig"} ;; "2.4.5" {"library" "zexy" "author" "zmoelng1"} ;; "2.4.6" {"library" "ouch" "author" "iembot"}} ;; "iemgui" {"1.42" {"library" "iemgui" "author" "tmusil"}}}}}) ;; ;; {"results": {"libraries": {<libname>: {<version>: [LIBRARY,...]}}}} ;; with LIBRARY like this ;; { ;; "library": <libname>, ;; "name": "zexy", ;; "description": "zexy-v0-0extended-(Darwin-i386-32)(Darwin-PowerPC-32)(Darwin-x86_64-32)-externals.tar.gz", ;; "author": "zmoelnig", ;; "timestamp": "2015-12-10 14:36:08", ;; "url": "http://puredata.info/Members/zmoelnig/software/zexy/0-0extended/zexy-v0-0extended-(Darwin-i386-32)(Darwin-PowerPC-32)(Darwin-x86_64-32)-externals.tar.gz", ;; "version": <version>, ;; "path": "http://puredata.info/Members/zmoelnig/software/zexy/0-0extended/", ;; "archs": [ ;; "Darwin-i386-32", ;; "Darwin-ppc-32", ;; "Darwin-amd64-32" ;; ] ;; } ;; which should then map to ;; { ;; "description": "zexy-v0-0extended-(Darwin-i386-32)(Darwin-PowerPC-32)(Darwin-x86_64-32)-externals.tar.gz", ;; "URL": "http://puredata.info/Members/zmoelnig/software/zexy/0-0extended/zexy-v0-0extended-(Darwin-i386-32)(Darwin-PowerPC-32)(Darwin-x86_64-32)-externals.tar.gz", ;; "uploader": "zmoelnig", ;; "timestamp": "2015-12-10 14:36:08", ;; "package": <libname>, ;; "version": <version>, ;; "architectures": [ ;; [ "Darwin", "i386", "32" ], ;; [ "Darwin", "PowerPC", "32"], ;; [ "Darwin", "x86_64", "32"] ;; ], ;; "extension": "tar.gz" ;; } ;; (get (get data "result") "libraries") (defn mangle-libdict [jlib] ;; "description" <- "description" ;; "URL" <- "url" ;; "uploader" <- "author" ;; "timestamp" <- "timestamp" ;; "package" <- "library" ;; "version" <- "version" ;; "architectures" <- ... ;; "extension" <- ... (setv jsonmap [ #("name" "package") #("version" "version") #("description" "description") #("url" "URL") #("author" "uploader") #("timestamp" "timestamp") ]) (setv result (dfor #(web locale) jsonmap locale (get jlib web))) (setv (get result "architectures") (lfor a (try-get jlib "archs" []) :if (bool a) (normalize-arch (split-archstring a)))) (setv (get result "extension") (try-get (parse-dekname (or (try-get jlib "url") "")) 3)) result) ;; mangle-libdict (try (lfor v (.values (try-get (try-get data "result" {}) "libraries" {})) l (.values v) lib l (mangle-libdict lib)) (except [e Exception] (log_error (% "Unable to parse JSON data: %s" #(e)))))) ;; parse-json-results (defn parse-data [data content-type] (cond (in "text/tab-separated-values" content-type) (parse-tab-separated-values data) (in "application/json" content-type) (parse-json-results data) True [])) (import requests) (setv r (requests.get searchurl :headers {"user-agent" (user-agent) "accept" "application/json, text/tab-separated-values"} :params {"libraries" libraries "objects" objects})) (if (= 200 r.status_code) (do (setv content-type (get r.headers "content-type")) (parse-data (if (in "application/json" content-type) (r.json) r.text) content-type)) (log.error (% "Searching '%s' failed with %s" #(searchurl r.status_code))))) (defn find-packages [searchterms ;; as returned by categorize-search-terms [architectures []] ;; a list of architecture tuples (e.g. [("Linux", "amd64", "32")]); defaults to 'native'; use ['*'] for any architecture [versioncount 0] ;; how many versions of a given library should be returned [searchurl default-searchurl] ;; where to search ] """find packages and filter them according to architecture, requirements and versioncount""" (log.debug "find-packages.search terms : %s" searchterms) (log.debug "find-packages.architectures: %s" architectures) (setv unversioned-libs (.difference (set (try-get searchterms "libraries" [])) (lfor x (try-get searchterms "versioned-libraries" []) (try-get x 0))) ) (log.debug "find-packages.unversioned : %s" unversioned-libs) (setv version-match? (make-requirements-matcher (try-get searchterms "versioned-libraries"))) (filter-older-versions (lfor x (or (search (or searchurl default-searchurl) (try-get searchterms "libraries" []) (try-get searchterms "objects" [])) [] ) :if (and (or (in (get x "package") unversioned-libs) (version-match? x)) (compatible-archs? (or architectures [(native-arch)]) (get x "architectures"))) x) versioncount)) (defn find [args] ;; TODO: this used to be '&optional args' """search the server for deken-packages and print the results""" (defn print-result [result] (setv url (get result "URL")) (setv description (get result "description")) (print (% "%s/%s uploaded by %s on %s for %s" #( (get result "package") (or (get result "version") "<unknown.version>") (get result "uploader") (get result "timestamp") (or (.join "/" (lfor x (get result "architectures") (.join "-" x))) "all architectures")))) (if (.endswith url description) None (print "\t" description)) (print "\t URL:" url) (print "\t" (* "-" 65)) (print "")) (setv both (= args.libraries args.objects)) (setv searchterms (categorize-search-terms (--packages-from-args-- args.search args.requirement) (or both args.libraries) (or both args.objects))) (setv version-match? (make-requirements-matcher (try-get searchterms "versioned-libraries"))) (lfor result (sort-searchresults (find-packages searchterms :architectures (if args.architecture (if (in "*" args.architecture) ["*"] (split-archstrings (.join "" (lfor a args.architecture (% "(%s)" a))))) [(native-arch)]) :versioncount (if (is args.depth None) (if (in "*" args.architecture) 0 1) args.depth) :searchurl (or args.search_url default-searchurl)) args.reverse) (or (print-result result) result))) ;; instruct the user how to manually upgrade 'deken' (defn upgrade [#* args] """print a big fat notice about manually upgrading via the webpage""" (defn open-webpage [page] (log.warning (% "Please manually check for updates on: %s" page)) (try (do (import webbrowser) (log.debug "Trying to open the page for you...") (webbrowser.open_new(page))) (except [e Exception]))) (open-webpage "https://github.com/pure-data/deken/tree/main/developer") (sys.exit "'update' not implemented for this platform!")) ;; verifies a dekfile by checking it's GPG-signature (if possible) the SHA256 ;; this require more thought: the verify function should never exit the program ;; (e.g. we want to remove downloaded files first) ;; return: True verification succeeded ;; None verification failed non-fatally (e.g. GPG-signature missing) ;; False verification failed (e.g. GPG-signature mismatch) ;; the 'gpg/hash' arg can modify the result: False: always return True ;; None: return True if None (defn verify [dekfile [gpgfile None] [hashfile None] [gpg True] [hash True]] """verify a dekfile by checking it's GPG-signature (if possible), resp. the SHA256; if gpg/hash is False, verification failure is ignored, if it's None the reference file is allowed to miss""" (defn verify-result [result fail errstring missstring] ;; result==True : OK ;; result==False: KO ;; result==None : verification failed (no signature file,...) ;; fail==True : fail on any error ;; fail==False : never fail ;; fail==None : only fail on verification errors (cond (is result None)(log.fatal missstring) (not result)(log.fatal errstring)) (cond (= fail False) True (and (is fail None) (is result None)) True True result)) (defn do-verify [verifun dekfile reffile extension fail [errstring "Verification of '%s' failed!"] [missstring "Verification file '%s' for '%s' is missing."]] (setv reference-file (or reffile (+ dekfile extension))) (if (or fail (os.path.exists reference-file)) (verify-result (verifun dekfile reference-file) fail (% errstring #( dekfile)) (% missstring #( reffile dekfile))) (or (log.info "Skipping verification with non-existing file '%s'" reference-file) True))) (setv vgpg (do-verify gpg-verify-file dekfile gpgfile ".asc" gpg "GPG-verification failed for '%s'" "GPG-signature '%s' missing for '%s'")) (setv vhash (do-verify (fn [dfile hfile] (hash-verify-file dfile hfile :algorithm "sha256")) dekfile hashfile ".sha256" hash "Hashsum mismatch for '%s'" "Hash file '%s' missing for '%s'")) (log.debug (% "GPG-verification : %s" #( vgpg))) (log.debug (% "hash-verification: %s" #( vhash))) (and vgpg vhash)) (defn download-verified [searchterms [architecture None] [verify-gpg True] [verify-hash True] [verify-none False] [search-url None] [keep-verification-files True] [download-dir "."]] """search for files using the <searchterms>, download any results and verify them. unverified files are removed (pending the verify-... flags) returns a tuple of a (list of verified files) and the number of failed verifications""" (defn try-download [url] (defn --verbose-download-- [url msg [warn-download-failure True]] (log.info msg) (setv outfile (download-file url :output-dir download-dir :warn-download-failure warn-download-failure)) (if outfile (log.info "Downloaded '%s'" outfile) (log.info "Failed to download '%s'" url)) outfile) (setv pkg (--verbose-download-- url "Downloading package")) (setv gpg (--verbose-download-- (+ url ".asc") "Downloading GPG signature" :warn-download-failure verify-gpg)) (setv hsh (--verbose-download-- (+ url ".sha256") "Downloading SHA256 hash" :warn-download-failure verify-hash)) (if (and (not (verify pkg gpg hsh :gpg verify-gpg :hash verify-hash)) (not verify-none)) (do (try-remove-file pkg) (try-remove-file gpg) (try-remove-file hsh) None) (do (log.info (% "Downloaded: %s" #( pkg))) (when (not keep-verification-files) (try-remove-file gpg) (try-remove-file hsh)) pkg))) (log.debug "download search terms : %s" searchterms) (log.debug "download architectures: %s" architecture) (log.debug "download have-terms : %s" (sum (lfor t ["libraries" "objects"] (len (.get searchterms t []))))) (setv foundurls (if (sum (lfor t ["libraries" "objects"] (len (.get searchterms t [])))) (lfor x (find-packages searchterms :architectures (when architecture (if (in "*" architecture) ["*"] (split-archstrings (.join "" (lfor a architecture (% "(%s)" a)))))) :versioncount 1 :searchurl search-url) :if (package-uri? (try-get x "URL" "")) (get x "URL")) [])) (log.debug "download found : %s" foundurls) (setv urls (lfor x (+ foundurls (try-get searchterms "urls" [])) :if (or (package-uri? x) (log.info (+ "Skipping non-package URL" x))) x)) (log.debug "download URLs : %s" urls) ;; return a list of successfully downloaded (and verified) files (setv result (lfor url urls (try-download url))) #( (list (filter None result)) (.count result None))) (defn install-package [pkgfile installdir] """unpack a <pkgfile> into <installdir>""" (log.info "Installing '%s' into '%s'" pkgfile installdir) (or (unzip-file pkgfile installdir) (untar-file pkgfile installdir))) (defn package [args] ;; are they asking to package a directory? (global default-floatsize) (setv default-floatsize args.default-floatsize) (lfor name args.source (if (os.path.isdir name) ;; if asking for a directory just package it up (archive-extra (archive-dir name (make-archive-name (os.path.normpath name) (os.path.basename (os.path.normpath (or args.name name))) args.version :output-dir args.output-dir :filenameversion args.dekformat :recurse-subdirs args.search-subdirs :extra-arch-files args.extra-arch-files)) (if (is args.objects None) name args.objects)) (fatal (% "Not a directory '%s'!" name))))) (defn uninstall [packages installdir] (import shutil) (lfor pkg packages :if pkg (do (setv pkgdir (os.path.join installdir pkg)) (if (os.path.isdir pkgdir) (do (log.info (% "Removing package directory '%s'" #( pkgdir))) (shutil.rmtree pkgdir True) pkgdir) (log.warning (% "Skipping non-existent directory '%s'" #( pkgdir))))))) ;; the executable portion of the different sub-commands that make up the deken tool (setv commands { ;; zip up a set of built externals :package (fn [args] (bool (package args))) ;; upload packaged external to pure-data.info :upload (fn [args] (defn set-nonempty-password [username password] (when password (try (do (import keyring) (keyring.set_password "deken" username password)) (except [e Exception] (log_warning e))))) (defn mk-pkg-ifneeded [x] (cond (os.path.isfile x) (if (archive? x) x (fatal (% "'%s' is not an externals archive!" x))) (os.path.isdir x) (do (import copy) (get (package (set-attr (copy.deepcopy args) "source" [x])) 0)) True (fatal (% "Unable to process '%s'!" x)))) (defn do-upload-username [packages destination username check-sources?] (upload-packages packages destination username (or destination.password (get-upload-password username args.ask-password)) check-sources?)) (defn do-upload [packages destination check-sources?] (do-upload-username packages destination (or destination.username (get-config-value "username") (prompt-for-value "username" (% "for %s://%s" #( (or destination.scheme default-destination.scheme) (or destination.hostname default-destination.hostname))))) check-sources?)) ;; do-upload returns the username (on success)... ;; so let's try storing the (non-empty) password in the keyring (setv userpass (do-upload (lfor x args.source (mk-pkg-ifneeded x)) (merge-url (urlparse (or (getattr args "destination") (get-config-value "destination" ""))) default-destination) (not args.source-error))) (set-nonempty-password #* userpass) (bool userpass)) ;; search for externals :find (fn [args] (bool (find args))) :search (fn [args] (bool (find args))) ;; verify downloaded files :verify (fn [args] (for [p args.dekfile] (when (and (os.path.isfile p) (not (verify p :gpg (and (not args.ignore-gpg) (if (or args.ignore-missing args.ignore-missing-gpg) None True)) :hash (and (not args.ignore-hash) (if (or args.ignore-missing args.ignore-missing-hash) None True))))) (fatal (% "Verification of '%s' failed" #( p))))) (bool (len args.dekfile))) ;; download a package (but don't install it) :download (fn [args] (setv packages (--packages-from-args-- args.package args.requirement)) (when (not packages) (fatal "Nothing to download!")) (not (get (download-verified ;; parse package specifiers (categorize-search-terms packages True False) :architecture (or args.architecture None) :verify-gpg (and (not args.ignore-gpg) (if (or args.ignore-missing args.ignore-missing-gpg) None True)) :verify-hash (and (not args.ignore-hash) (if (or args.ignore-missing args.ignore-missing-hash) None True)) :verify-none (not args.verify) :search-url args.search-url :keep-verification-files args.keep-files :download-dir args.output-dir) 1))) :uninstall (fn [args] (when args.self (fatal "self-'uninstall' not implemented for this platform!")) (any (filter None (uninstall (--packages-from-args-- args.package args.requirement) args.installdir)))) :install (fn [args] (when (and (not args.package) (not args.requirement)) (fatal "self-'install' not implemented for this platform!")) (defn install-pkgs [pkgs installdir] (when pkgs (try (os.makedirs installdir) (except [e Exception] (when (not (os.path.isdir installdir)) (raise e)))) (lfor pkg pkgs :if (or (os.path.isfile pkg) (log.warning (% "Skipping non-existing file '%s'" #( pkg)))) (install-package pkg installdir)))) (setv pkgs (--packages-from-args-- args.package args.requirement)) ;; those search-terms that refer to local files (setv file-pkgs (sfor x pkgs :if (and (package-uri? x) (os.path.exists x)) x)) ;; search/download/verify the rest (setv pkgs (.difference pkgs (set file-pkgs))) (setv downloaded-pkgs (if pkgs (download-verified (categorize-search-terms pkgs :objects False) :architecture (or args.architecture None) :verify-gpg (and (not args.ignore-gpg) (if (or args.ignore-missing args.ignore-missing-gpg) None True)) :verify-hash (and (not args.ignore-hash) (if (or args.ignore-missing args.ignore-missing-hash) None True)) :verify-none (not args.verify) :search-url args.search-url :keep-verification-files args.keep-files :download-dir args.installdir) #( [] 0))) (if (install-pkgs (.union file-pkgs (set (get downloaded-pkgs 0))) args.installdir) (do (when (not args.keep-files) (for [f (get downloaded-pkgs 0)] (try-remove-file f))) (not (get downloaded-pkgs 1))) False)) :systeminfo print-system-info ;; the rest should have been caught by the wrapper script :systemfix (fn [args] (setv fixes { :easywebdav2 (fn [] (import easywebdav2) (fix-easywebdav2 easywebdav2 :exit None) True)}) (setv fixnames (lfor k fixes k.name)) (defn try-call [x] (when x (x))) (when args.all (if args.fix (fatal "'--all' and named fixes are exclusive. Choose one.") (setv args.fix fixnames))) (if args.fix (all (lfor f args.fix (try-call (.get fixes (hy.models.Keyword f))))) (fatal (% "Known systemfixes: %s" (.join "," fixnames)) 0))) :update upgrade :upgrade upgrade}) ;; kick things off by using argparse to check out the arguments supplied by the user (defn main [] """run deken""" (defn --get-boolean-config-value-- [name [default None]] (setv v (get-config-value "sign-gpg" None)) (if (is v None) default (str-to-bool v))) (setv default-sign-gpg (--get-boolean-config-value-- "sign-gpg" True)) (setv default-debug False) (setv default-search-subdirs False) (setv default-source-error True) (setv default-output-dir ".") (setv default-verify (--get-boolean-config-value-- "verify" True)) (setv default-ignore-missing (--get-boolean-config-value-- "ignore-missing" None)) (setv default-ignore-gpg (--get-boolean-config-value-- "ignore-gpg" None)) (setv default-ignore-missing-gpg (--get-boolean-config-value-- "ignore-missing-gpg" None)) (setv default-ignore-hash (--get-boolean-config-value-- "ignore-hash" None)) (setv default-ignore-missing-hash (--get-boolean-config-value-- "ignore-missing-hash" None)) (setv default-keep-files (--get-boolean-config-value-- "keep-files" False)) (defn parse-args [parser] (setv args (.parse_args parser)) (log.setLevel (max 1 (+ logging.WARN (* 10 (- args.quiet args.verbose))))) (del args.verbose) (del args.quiet) ;; rewrite some functions, based on the args ;; no-sign-gpg (when (not (getattr args "sign_gpg" default-sign-gpg)) (global gpg-sign-file) (defn gpg-sign-file [filename])) ;; debug (when args.debug (global log_exception) (defn log_exception [] (log.exception ""))) ;; practically no GPG signatures are verifiable ;; - there are *very* few ;; - those that are there, are not available in any public keyring ;; so the default is to *not* verify GPG-signatures ;; (unless explicitly requested via --verify or --no-ignore-gpg) (when (is None (getattr args "ignore_gpg" None)) (setv args.ignore-gpg (if (is None (getattr args "verify" None)) True args.verify))) args) (defn add-arg-yesno [parser flag default helpyes helpno] (parser.add_argument (% "--%s" flag) :action "store_true" :default default :help (% "%s (DEFAULT: %s)." #( helpyes default)) :required False) (parser.add_argument (% "--no-%s" flag) :action "store_false" :dest (.replace flag "-" "_") :help helpno :required False)) (defn add-noverify-flags [parser] (add-arg-yesno parser "ignore-gpg" default-ignore-gpg "Ignore an invalid (or no) GPG-signature" "Don't ignore invalid GPG-signatures") (add-arg-yesno parser "ignore-hash" default-ignore-hash "Ignore an unverifiable hashsum" "Fail if the hashsum is unverifiable") (add-arg-yesno parser "ignore-missing" default-ignore-missing "Don't fail if detached verification files are missing" "Fail if detached verification files are missing") (add-arg-yesno parser "ignore-missing-gpg" default-ignore-missing-gpg "Don't fail if there is no GPG-signature. Overrides '--ignore-missing'" "Fail if there is no GPG-signature") (add-arg-yesno parser "ignore-missing-hash" default-ignore-missing-hash "Don't fail if there is no hashsum file" "Fail if there is no hashsum file")) (defn add-search-flags [parser] (parser.add_argument "--search-url" :help "URL to query for deken-packages" :default "" :required False) (parser.add_argument "--architecture" "--arch" :help (% "Filter architectures; use '*' for all architectures (DEFAULT: %s)" #( (.join "-" (native-arch)))) :action "append" :default [] :required False) (parser.add_argument "--requirement" "-r" :action "append" :default [] :help "Install/find/download from the given requirements file. This option can be used multiple times.")) (defn add-package-flags [parser] (parser.add_argument "--name" "-n" :help "The library name as it appears in the package filename (DEFAULT: the last path component of the SOURCE)." :default None :required False) (parser.add_argument "--version" "-v" :help "A library version number to insert into the package name (in case the package is created)." :default None :required False) (parser.add_argument "--objects" :help "Specify a tsv-file that lists all the objects of the library (DEFAULT: generate it)." :default None :required False) (add-arg-yesno parser "search-subdirs" default-search-subdirs "EXPERT: Search subdirectories for externals to determine architecture string" "EXPERT: Only search the given directory for externals to determine architecture string (without descending into subdirectories).") (parser.add_argument "--extra-arch-files" :help "EXPERT: Additionally take the given files into account for determining the package architecture (DEFAULT: use externals found in the package directory)." :default [] :nargs "*" :required False) (parser.add_argument "--output-dir" :help "Output directory for package files (DEFAULT: .)." :default "." :required False) (parser.add_argument "--default-floatsize" :help "EXPERT: Use the given float-size if it cannot be determined automatically. Use with care! (DEFAULT: None)." :default None :type int :choices [0 32 64] :required False) (parser.add_argument "--dekformat" :help "Override the deken packaging format, in case the package is created (DEFAULT: 1)." :type int :choices [0 1] :default 1 :required False) (add-arg-yesno parser "sign-gpg" default-sign-gpg "Sign the package" "Do not sign the package")) (defn add-find-flags [parser] (add-search-flags parser) (parser.add_argument "--depth" :help "Limit search result to the N last versions (0 is unlimited; DEFAULT: 1)" :default None :type int :required False) (parser.add_argument "--reverse" :action "store_true" :help "Reverse search result sorting" :required False) (parser.add_argument "--libraries" :action "store_true" :help "Find libraries (DEFAULT: True)" :required False) (parser.add_argument "--objects" :action "store_true" :help "Find objects (DEFAULT: True)" :required False) (parser.add_argument "search" :nargs "*" :metavar "TERM" :help "Libraries/objects to search for")) (setv arg-parser (argparse.ArgumentParser :prog "deken" :description "Deken is a packaging tool for Pure Data externals.")) (setv arg-subparsers (arg-parser.add_subparsers :dest "command" :metavar "{package,upload,find,download,verify,install,uninstall,systeminfo,systemfix}" )) (setv arg-package (arg-subparsers.add_parser "package" :description "create (and sign) a DEK-package from a directory with externals/abstractions/... guessing the architecture(s)" )) (setv arg-upload (arg-subparsers.add_parser "upload" :description "upload a DEK-package to the deken repository (eventually creating the package from a directory)" )) (setv arg-find (arg-subparsers.add_parser "find" :description "find packages (and/or libraries containing objects) in the repository" )) (setv arg-search (arg-subparsers.add_parser "search" :description "find packages (and/or libraries containing objects) in the repository" )) ;; verify a downloaded package (both SHA256 and (if available GPG)) (setv arg-verify (arg-subparsers.add_parser "verify" :description "verify a downloaded package (using SHA256 checksums and - if available - GPG)")) ;; download a package from the internet (setv arg-download (arg-subparsers.add_parser "download" :description "search for a package, download it and verify the download")) ;; install a package from the internet ;; - package can be either an URL, a local file or a search string ;; - packages are verified (SHA256/GPG) ;; - search is similar to "find", but requires an "exact match" ;; and installs only the first match (with the highest version number) (setv arg-install (arg-subparsers.add_parser "install" :description "search for a package, download and verify it and install it to be used by Pd")) (setv arg-uninstall (arg-subparsers.add_parser "uninstall" :description "attempt to uninstall (delete) an installed package")) (setv arg-upgrade (arg-subparsers.add_parser "upgrade" :description "self-'update' deken.")) (setv arg-update (arg-subparsers.add_parser "update" :description "self-'update' deken.")) (arg-subparsers.add_parser "systeminfo" :description "print information about your deken installation.") (setv arg-systemfix (arg-subparsers.add_parser "systemfix" :description "run system-fixups (e.g. patching some python modules)" )) (arg-parser.add_argument "-v" "--verbose" :help "Raise verbosity" :action "count" :default 0) (arg-parser.add_argument "-q" "--quiet" :help "Lower verbosity" :action "count" :default 0) (arg-parser.add_argument "--debug" :help (% "Enable debugging output (DEFAULT: %s)" default-debug) :default default-debug :action "store_true" :required False) (arg-parser.add_argument "--no-debug" :help "Disable debugging output" :dest "debug" :action "store_false" :required False) (arg-parser.add_argument "--version" :action "version" :version version :help "Outputs the version number of Deken and exits.") (arg-parser.add_argument "--platform" :action "version" :version (.join "-" (native-arch)) :help "Outputs a guess of the current architecture and exits.") (arg-package.add_argument "source" :nargs "+" :metavar "SOURCE" :help "The path to a directory of externals, abstractions, or GUI plugins to be packaged.") (add-package-flags arg-package) (arg-upload.add_argument "source" :nargs "+" :metavar "PACKAGE" :help "The path to a package file to be uploaded, or a directory which will be packaged first automatically.") (add-package-flags arg-upload) (arg-upload.add_argument "--destination" "-d" :help (% "The destination folder to upload the package to (DEFAULT: %s)." (.replace default-destination.path "%" "%%")) :default "" :required False) (arg-upload.add_argument "--ask-password" "-P" :action "store_true" :help "Ask for upload password (rather than using a password-manager)." :default "" :required False) (add-arg-yesno arg-upload "source-error" default-source-error "Prevent uploading of packages without sources" "Force-allow uploading of packages without sources") (add-find-flags arg-find) (add-find-flags arg-search) (add-noverify-flags arg-verify) (arg-verify.add_argument "dekfile" :nargs "*" :help "deken package to verify") (add-search-flags arg-download) (add-arg-yesno arg-download "verify" default-verify "Abort download on verification errors" "Don't abort download on verification errors") (add-noverify-flags arg-download) (add-arg-yesno arg-download "keep-files" default-keep-files "Keep verification files after downloading them" "Remove verification files after downloading them") (arg-download.add_argument "--output-dir" :default default-output-dir :help (% "Output directory for downloaded package files (DEFAULT: %s)." default-output-dir)) (arg-download.add_argument "package" :nargs "*" :help "Package specifier or URL to download") (add-search-flags arg-install) (add-arg-yesno arg-install "verify" default-verify "Abort download/installation on verification errors" "Don't abort download/installation on verification errors") (add-noverify-flags arg-install) (arg-install.add_argument "--install-dir" :default default-installpath :dest "installdir" :help (% "Target directory to install packages to (DEFAULT: %s)" default-installpath)) (arg-install.add_argument "--installdir" :help argparse.SUPPRESS) (add-arg-yesno arg-install "keep-files" default-keep-files "Keep files after downloading them" "Remove files after downloading them") (arg-install.add_argument "--self" :action "store_true" :help "(Re)install the 'deken' cmdline-utility (and dependencies) itself (ignores all other arguments)") (arg-install.add_argument "package" :nargs "*" :help "Package specifier or URL to install") (arg-update.add_argument "--self" :action "store_true" :required True :help "Update the 'deken' cmdline-utility (and dependencies) itself (ignores all other arguments)") (arg-upgrade.add_argument "--self" :action "store_true" :required True :help "Update the 'deken' cmdline-utility (and dependencies) itself (ignores all other arguments)") (arg-uninstall.add_argument "--requirement" "-r" :action "append" :default [] :help "Uninstall packages specified in the given requirements file. This option can be used multiple times.") (arg-uninstall.add_argument "--install-dir" :default default-installpath :dest "installdir" :help (% "Directory to find installed packages (DEFAULT: %s)" default-installpath)) (arg-uninstall.add_argument "--installdir" :help argparse.SUPPRESS) (arg-uninstall.add_argument "--self" :action "store_true" :help "Remove the 'deken' cmdline-utility (and dependencies) itself (ignores all other arguments)") (arg-uninstall.add_argument "package" :nargs "*" :help "Package to uninstall") (arg-systemfix.add_argument "--all" :action "store_true" :help "Run all system-fixes") (arg-systemfix.add_argument "fix" :metavar "FIX" :nargs "*" :help "Run the named system-fix") (setv arguments (parse-args arg-parser)) (setv command (.get commands (hy.models.Keyword arguments.command))) ;;(print "Deken" version) (log.debug (.join " " sys.argv)) (if command (command arguments) (.print_help arg-parser))) (when (= __name__ "__main__") (try (sys.exit (not (main))) (except [e KeyboardInterrupt] (log_warning "\n[interrupted by user]")))) ���������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������deken-0.10.4/developer/deken.spec�������������������������������������������������������������������0000664�0000000�0000000�00000003677�14764245604�0016642�0����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������# -*- mode: python -*- import os block_cipher = None datas = [('deken.hy', '.')] versionfile = "DEKEN_VERSION" try: import tempfile tmpdir = tempfile.mkdtemp() version = os.environ['DEKEN_VERSION'] versionfile = os.path.join(tmpdir, versionfile) with open(versionfile, 'w') as f: f.write(version) datas += [(versionfile, '.')] except Exception as e: print("OOPS: %s" % (e,)) versionfile = None def easywebdav2_patch1(): try: import easywebdav2 print("trying to fix 'easywebdav2'") A=""" for dir_ in dirs:\n try:\n self.mkdir(dir, safe=True, **kwargs)""" B=""" for dir_ in dirs:\n try:\n self.mkdir(dir_, safe=True, **kwargs)""" filename = os.path.join(os.path.dirname(easywebdav2.__file__), 'client.py') print(filename) with open(filename, "r") as f: data = f.read() data = data.replace(A, B) with open(filename, "w") as f: f.write(data) except Exception as e: print("FAILED to patch 'easywebdav2', continuing anyhow...\n %s" % (e)) easywebdav2_patch1() a = Analysis(['pydeken.py'], pathex=['.'], binaries=[], datas=datas, hiddenimports=[], hookspath=['installer/'], runtime_hooks=[], excludes=[], win_no_prefer_redirects=False, win_private_assemblies=False, cipher=block_cipher) pyz = PYZ(a.pure, a.zipped_data, cipher=block_cipher) exe = EXE(pyz, a.scripts, a.binaries, a.zipfiles, a.datas, name='deken', debug=False, strip=False, upx=True, runtime_tmpdir=None, console=True ) try: import shutil shutil.rmtree(tmpdir) except Exception as e: print("OOPS: %s" % (e,)) print("BYE.") �����������������������������������������������������������������deken-0.10.4/developer/installer/�������������������������������������������������������������������0000775�0000000�0000000�00000000000�14764245604�0016660�5����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������deken-0.10.4/developer/installer/hook-elftools.py���������������������������������������������������0000664�0000000�0000000�00000001053�14764245604�0022016�0����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������# -*- mode: python -*- #----------------------------------------------------------------------------- # Copyright (c) 2017, PyInstaller Development Team. # # Distributed under the terms of the GNU General Public License with exception # for distributing bootloader. # # The full license is in the file COPYING.txt, distributed with this software. #----------------------------------------------------------------------------- # Hook for the elftools module: https://github.com/eliben/pyelftools hiddenimports = ['elftools.elf', 'elftools.elf.elffile'] �������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������deken-0.10.4/developer/installer/hook-hy.py���������������������������������������������������������0000664�0000000�0000000�00000001136�14764245604�0020611�0����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������# -*- mode: python -*- #----------------------------------------------------------------------------- # Copyright (c) 2017, PyInstaller Development Team. # # Distributed under the terms of the GNU General Public License with exception # for distributing bootloader. # # The full license is in the file COPYING.txt, distributed with this software. #----------------------------------------------------------------------------- # Hook for the uniseg module: https://pypi.python.org/pypi/uniseg from PyInstaller.utils.hooks import collect_data_files datas = collect_data_files('hy', include_py_files=True) ����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������deken-0.10.4/developer/installer/hook-macholib.py���������������������������������������������������0000664�0000000�0000000�00000001066�14764245604�0021751�0����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������# -*- mode: python -*- #----------------------------------------------------------------------------- # Copyright (c) 2017, PyInstaller Development Team. # # Distributed under the terms of the GNU General Public License with exception # for distributing bootloader. # # The full license is in the file COPYING.txt, distributed with this software. #----------------------------------------------------------------------------- # Hook for the macholib module: https://bitbucket.org/ronaldoussoren/macholib hiddenimports = ['macholib.MachO', 'macholib.SymbolTable'] ��������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������deken-0.10.4/developer/pydeken.py�������������������������������������������������������������������0000775�0000000�0000000�00000007617�14764245604�0016712�0����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������#!/usr/bin/env python # -*- coding: utf-8 -*- # This software is copyrighted by Chris McCormick and others. The following # terms (the "Standard Improved BSD License") apply to all files associated # with the software unless explicitly disclaimed in individual files: # # Redistribution and use in source and binary forms, with or without # modification, are permitted provided that the following conditions are # met: # # 1. Redistributions of source code must retain the above copyright # notice, this list of conditions and the following disclaimer. # 2. Redistributions in binary form must reproduce the above # copyright notice, this list of conditions and the following # disclaimer in the documentation and/or other materials provided # with the distribution. # 3. The name of the author may not be used to endorse or promote # products derived from this software without specific prior # written permission. # # THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY # EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, # THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A # PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR # BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, # EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED # TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, # DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND # ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT # LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING # IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF # THE POSSIBILITY OF SUCH DAMAGE. # this is a thin wrapper around deken.hy, to launch it without a # HY executable (or file associations) try: import argparse import copy import getpass import os import re import sys import tarfile import zipfile import datetime import elftools import macholib import pefile import gnupg import hashlib import requests import subprocess except ImportError: pass # 'keyring' is disabled here as it makes problems with pyinstaller try: import keyring.backends.Windows import keyring pass except ImportError: pass try: import webbrowser except ImportError: pass try: import easywebdav2 except ImportError: try: import easywebdav except ImportError: pass try: import ConfigParser import StringIO import urlparse except ImportError: import configparser import io import urllib.parse import hy import deken ## on macOS, pyinstaller requires more help... try: import runpy import hy.core.bootstrap import hy.contrib.loop except ImportError: pass def askpass(prompt="Password: ", fallback=None): try: subprocess.call(["stty", "-echo"]) except: if fallback: return fallback(prompt) return None sys.stdout.write(prompt) sys.stdout.flush() password = None try: try: password = raw_input() except NameError: password = input() except: pass sys.stdout.write("\n") sys.stdout.flush() subprocess.call(["stty", "echo"]) return password def resource_path(relative_path): """Get absolute path to resource, works for dev and for PyInstaller""" try: # PyInstaller creates a temp folder and stores path in _MEIPASS base_path = sys._MEIPASS except Exception: base_path = os.path.abspath(".") return os.path.join(base_path, relative_path) if __name__ == "__main__": try: with open(resource_path("DEKEN_VERSION"), "r") as f: version = f.read().strip() deken.version = version except OSError: pass except IOError: pass deken_askpass = deken.askpass deken.askpass = lambda prompt: askpass(prompt, deken_askpass) deken.main() �����������������������������������������������������������������������������������������������������������������deken-0.10.4/developer/release.sh�������������������������������������������������������������������0000775�0000000�0000000�00000002765�14764245604�0016654�0����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������#!/bin/sh ## deken versioning scheme # - we losely follow semver # - releases should *always* have an even bugfix-number # - during development, we use an odd bugfix-number # files that contain the version number # - ./developer/deken # - ./deken-plugin.tcl # - ./README.deken.pd # - .git-ci/deken-test/README.deken.txt version_script=".git-ci/git-version" cd ${0%/*}/.. version=$1 version=${version#v} if [ "x${version}" = "x" ]; then echo "usage: $0 <version>" 1>&2 exit 1 fi version_check() { local bugfix bugfix=$3 if [ -n "${bugfix}" ] && [ $((bugfix % 2)) -ne 0 ]; then return 1 fi return 0 } if git diff --name-only | sed -e 's|^|CHANGED: |' | tee /dev/stderr | grep . >/dev/null; then echo "the repository contains changes!" 1>&2 echo "commit or stash them, before releasing." 1>&2 exit 1 fi if [ -x "${version_script}" ]; then echo "setting version to ${version}" ${version_script} "${version}" else echo "version-script '${version_script}' missing" exit 1 fi if version_check $(echo $version | sed -e 's|[.-]| |g'); then echo "committing changes and tagging release" 1>&2 git commit \ developer/deken deken-plugin.tcl \ README.deken.pd .git-ci/deken-test/README.deken.txt \ -m "Releasing v${version}" \ && git tag -s -m "Released deken-v${version}" "v${version}" else echo "committing changes for development version" 1>&2 git commit \ developer/deken deken-plugin.tcl \ README.deken.pd .git-ci/deken-test/README.deken.txt \ -m "Bump dev-version to v${version}" fi �����������deken-0.10.4/developer/requirements.txt�������������������������������������������������������������0000664�0000000�0000000�00000000150�14764245604�0020143�0����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������hy~=1.0 easywebdav2~=1.3 keyring~=23.5 macholib~=1.16 pefile~=2021.9 pyelftools~=0.28 python-gnupg~=0.4 ������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������deken-0.10.4/developer/setup.py���������������������������������������������������������������������0000775�0000000�0000000�00000013677�14764245604�0016416�0����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������#!/usr/bin/env python3 # -*- coding: utf-8 -*- import sys import os from setuptools import setup def check_pyversions(versions): """checks if the python-version is within the list of versions to check is we are running py2 or py3: check_pyversions(((2,), (3,))) to check if we are running either py2 or py3.4.2 (but no other py3 version) check_pyversions(((2,), (3,4,2))) """ for v in versions: if all([x == y for x, y in zip(v, sys.version_info)]): return True return False # ModuleFinder can't handle runtime changes to __path__, but win32com uses them try: # py2exe 0.6.4 introduced a replacement modulefinder. # This means we have to add package paths there, not to the built-in # one. If this new modulefinder gets integrated into Python, then # we might be able to revert this some day. # if this doesn't work, try import modulefinder try: import py2exe.mf as modulefinder except ImportError: import modulefinder import win32com for p in win32com.__path__[1:]: modulefinder.AddPackagePath("win32com", p) for extra in ["win32com.shell"]: # ,"win32com.mapi" __import__(extra) m = sys.modules[extra] for p in m.__path__[1:]: modulefinder.AddPackagePath(extra, p) import py2exe except ImportError: # no build path setup, no worries. pass # This is a list of files to install, and where # (relative to the 'root' dir, where setup.py is) # You could be more specific. files = [ "pydeken.py", ] data_files = [ "deken.hy", ] setup_requires = [] dist_dir = "" dist_file = None options = {} setupargs = { "name": "deken", "version": "0.2.4", "description": """Pure Data externals wrangler""", "author": "Chris McCormick, IOhannes m zmölnig et al.", "author_email": "pd-list@lists.puredata.info", "url": "https://git.iem.at/pure-data/deken", # Name the folder where your packages live: # (If you have other packages (dirs) or modules (py files) then # put them into the package directory - they will be found # recursively.) # 'packages': ['deken', ], # This dict maps the package name =to=> directories # It says, package *needs* these files. "package_data": {"deken": files}, "data_files": data_files, "install_requires": [ # 'PySide', ], "scripts": ["deken", "pydeken.py"], "long_description": """ a tool to create and upload dek packages to puredata.info, so they can be installed by Pd's built-in package manager """, # # https://pypi.python.org/pypi?%3Aaction=list_classifiers "classifiers": [ "Development Status :: 3 - Alpha", "Environment :: Console", "Intended Audience :: Developers", "License :: OSI Approved :: BSD 3 clause", "Natural Language :: English", # "Topic :: Internet :: WWW/HTTP :: Site Management", ], "options": options, "setup_requires": setup_requires, } if "py2exe" in sys.argv: dist_dir = "%s-%s" % (setupargs["name"], setupargs["version"]) dist_file = "%s.exe-%s.zip" % (setupargs["name"], setupargs["version"]) setup_requires += [ "py2exe", ] def getMSVCfiles(): # urgh, find the msvcrt redistributable DLLs # either it's in the MSVC90 application folder # or in some winsxs folder from glob import glob program_path = os.path.expandvars("%ProgramFiles%") winsxs_path = os.path.expandvars("%SystemRoot%\WinSXS") msvcrt_paths = [ ( r"%s\Microsoft Visual Studio 9.0\VC\redist\x86\Microsoft.VC90.CRT" % program_path ) ] if check_pyversions(((2, 7),)): # python2.7 seems to be built against VC90 (9.0.21022), # so let's try that msvcrt_paths += glob( r"%s\x86_microsoft.vc90.crt_*_9.0.21022.8_*_*" "\\" % winsxs_path ) for p in msvcrt_paths: if os.path.exists(os.path.join(p, "msvcp90.dll")): sys.path.append(p) f = glob(r"%s\*.*" % p) if f: return [("Microsoft.VC90.CRT", f)] return None return None def getRequestsCerts(): import requests.certs f = requests.certs.where() if f: return [(".", [f])] data_files += getMSVCfiles() or [] data_files += getRequestsCerts() or [] print(data_files) setupargs["windows"] = [ { # 'icon_resources': [(1, "media\deken.ico")], "script": "pydeken.py", } ] setupargs["zipfile"] = None options["py2exe"] = { "includes": [ "deken.hy", ], "packages": [ "requests", ], "bundle_files": 3, "dist_dir": os.path.join("dist", dist_dir), } if "py2app" in sys.argv: dist_dir = "%s.app" % (setupargs["name"],) dist_file = "%s.app-%s.zip" % (setupargs["name"], setupargs["version"]) setup_requires += [ "py2app", ] setupargs["app"] = [ "pydeken.py", ] options["py2app"] = { "packages": [ "requests", ], } if dist_dir: try: os.makedirs(os.path.join("dist", dist_dir)) except FileExistsError: pass setup(**setupargs) if dist_dir and dist_file: import zipfile def zipdir(path, ziph): # ziph is zipfile handle for root, dirs, files in os.walk(path): for f in files: fullname = os.path.join(root, f) archname = os.path.relpath(fullname, os.path.join(path, "..")) if ziph: ziph.write(fullname, archname) else: print("%s -> %s" % (fullname, archname)) with zipfile.ZipFile(dist_file, "w", compression=zipfile.ZIP_DEFLATED) as myzip: zipdir(os.path.join("dist", dist_dir), myzip) ���������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������