|
| 1 | +import subprocess as sp |
| 2 | +import os |
| 3 | +import os.path as osp |
| 4 | +import shutil |
| 5 | +import sys |
| 6 | +import re |
| 7 | +import datetime |
| 8 | +import dateutil.parser |
| 9 | +from common_functions import print_separator |
| 10 | + |
| 11 | +class LilyCmd(object): |
| 12 | + """This class represents a lilypond command and provides some |
| 13 | + facilities to |
| 14 | +
|
| 15 | + - run LilyPond |
| 16 | + - install LilyPond versions from the internet into a local cache |
| 17 | + - manage the cache, cleaning old versions |
| 18 | +
|
| 19 | + """ |
| 20 | + |
| 21 | + # Download site for LilyPond distributions |
| 22 | + binary_site = "http://download.linuxaudio.org/lilypond/binaries/" |
| 23 | + # String template for generating the LilyPond installation command |
| 24 | + lily_install_script = "lilypond-install.sh" |
| 25 | + |
| 26 | + # After this amount of time a LilyPond version will be removed by |
| 27 | + # a call to LilyCmd.clean_cache() |
| 28 | + cache_cleanup_interval = datetime.timedelta(days=1) |
| 29 | + |
| 30 | + # Root directory for the cache |
| 31 | + cache_root = osp.join(os.getenv('HOME'), '.lilypond') |
| 32 | + |
| 33 | + def __init__(self, command_path, cached): |
| 34 | + self.command = command_path |
| 35 | + self.cached = cached |
| 36 | + try: |
| 37 | + self.version = self._lilypond_version() |
| 38 | + self.installed = True |
| 39 | + except: |
| 40 | + self.installed = False |
| 41 | + |
| 42 | + #################################################################### |
| 43 | + # Public methods |
| 44 | + |
| 45 | + @staticmethod |
| 46 | + def system(cmd_path='lilypond'): |
| 47 | + """Get a new instance of Lilypond provided by the |
| 48 | + system. (eg. /usr/bin/lilypond)""" |
| 49 | + return LilyCmd(cmd_path, cached=False) |
| 50 | + |
| 51 | + @classmethod |
| 52 | + def with_version(cls, platform, version): |
| 53 | + """Get a new version of Lilypond, given the platform and the |
| 54 | + version. |
| 55 | +
|
| 56 | + **Note**: This command does not install LilyPond in the local |
| 57 | + cache. To check if the instance returned by this method |
| 58 | + corresponds to an actually installed LilyPond version, check |
| 59 | + the `installed` attribute. The reason for not installing |
| 60 | + LilyPond with this command is that we may be interested in all |
| 61 | + kind of ancillary information (like the cache directory) |
| 62 | + without actually installing LilyPond. |
| 63 | +
|
| 64 | + """ |
| 65 | + lily_cmd_path = osp.join( |
| 66 | + LilyCmd._cache_directory(platform, version), |
| 67 | + 'bin', 'lilypond') |
| 68 | + lily_cmd = LilyCmd(lily_cmd_path, cached=True) |
| 69 | + return lily_cmd |
| 70 | + |
| 71 | + def execute(self, args): |
| 72 | + """Executes the LilyPond command with the given arguments""" |
| 73 | + self._mark_cache() |
| 74 | + lily = sp.Popen([self.command] + args, |
| 75 | + stdout=sp.PIPE, stderr=sp.PIPE) |
| 76 | + (out, err) = lily.communicate() |
| 77 | + return lily.returncode, out, err |
| 78 | + |
| 79 | + @classmethod |
| 80 | + def install(cls, platform, version): |
| 81 | + """Download and install LilyPond version if not cached""" |
| 82 | + lilypond_cmd = LilyCmd.with_version(platform, version) |
| 83 | + print "\nChecking LilyPond presence" |
| 84 | + if lilypond_cmd.installed: |
| 85 | + print ("LilyPond {} is already installed in cache," \ |
| 86 | + +" continuing with test script.").format( |
| 87 | + lilypond_cmd.version) |
| 88 | + else: |
| 89 | + print "LilyPond {} is not installed yet.".format(version) |
| 90 | + print "Downloading and installing now" |
| 91 | + sp.check_call( |
| 92 | + ["wget", "-O", |
| 93 | + cls.lily_install_script, |
| 94 | + cls._download_url(platform, version)]) |
| 95 | + sp.check_call(["sh", cls.lily_install_script, |
| 96 | + "--prefix", |
| 97 | + LilyCmd._cache_directory(platform, version), |
| 98 | + "--batch"]) |
| 99 | + |
| 100 | + @classmethod |
| 101 | + def clean_cache(cls): |
| 102 | + """Clean the cache from versions of Lilypond older than |
| 103 | + `cache_cleanup_interval`""" |
| 104 | + print "Clean cache\n" |
| 105 | + cached = cls._get_cached_versions() |
| 106 | + now = datetime.datetime.now() |
| 107 | + for lily in cached: |
| 108 | + if lily['last_used'] is None: |
| 109 | + print 'Removing cached LilyPond', lily['version'],\ |
| 110 | + lily['platform'], '(never used)' |
| 111 | + shutil.rmtree(lily['directory']) |
| 112 | + elif now - lily['last_used'] > cls.cache_cleanup_interval: |
| 113 | + print 'Removing cached LilyPond', lily['version'],\ |
| 114 | + lily['platform'], '(last used', \ |
| 115 | + lily['last_used'].isoformat(), ')' |
| 116 | + shutil.rmtree(lily['directory']) |
| 117 | + else: |
| 118 | + print 'Keeping cached LilyPond', lily['version'],\ |
| 119 | + lily['platform'], '(last used', \ |
| 120 | + lily['last_used'].isoformat(), ')' |
| 121 | + |
| 122 | + #################################################################### |
| 123 | + # Private members |
| 124 | + |
| 125 | + @classmethod |
| 126 | + def _cache_directory(cls, platform, version): |
| 127 | + """Get the cache directory name for the given platform and version""" |
| 128 | + return osp.join(cls.cache_root, platform, version) |
| 129 | + |
| 130 | + def _lilypond_version(self): |
| 131 | + """Determine the LilyPond version actually run |
| 132 | + by the command self.lily_command""" |
| 133 | + lily = sp.Popen([self.command, "--version"], |
| 134 | + stdout=sp.PIPE, stderr=sp.PIPE) |
| 135 | + version_line = lily.communicate()[0].splitlines()[0] |
| 136 | + return re.search(r"\d+\.\d+\.\d+", version_line).group(0) |
| 137 | + |
| 138 | + def _mark_cache_file(self): |
| 139 | + """Get the name of the file that will store the timestamp of the last |
| 140 | + time this command has been used.""" |
| 141 | + if self.cached: |
| 142 | + return osp.join(osp.dirname(self.command), '.oll-last-used') |
| 143 | + else: |
| 144 | + return None |
| 145 | + |
| 146 | + def _mark_cache(self): |
| 147 | + """Write the timestamp of now in the cache file""" |
| 148 | + if self.cached: |
| 149 | + with open(self._mark_cache_file(), 'w') as mark_file: |
| 150 | + mark_file.write(datetime.datetime.now().isoformat()) |
| 151 | + |
| 152 | + def _last_used(self): |
| 153 | + """Returns the last time this command has been used, or None if it was |
| 154 | + never used.""" |
| 155 | + if self.cached: |
| 156 | + if not osp.isfile(self._mark_cache_file()): |
| 157 | + return None |
| 158 | + with open(self._mark_cache_file(), 'r') as mark_file: |
| 159 | + fcontent = mark_file.readline() |
| 160 | + return dateutil.parser.parse(fcontent) |
| 161 | + |
| 162 | + |
| 163 | + @classmethod |
| 164 | + def _download_url(cls, lily_platform, lily_version): |
| 165 | + """Format a string representing the URL to |
| 166 | + download the requested LilyPond distribution""" |
| 167 | + return "{}{}/lilypond-{}.{}.sh".format( |
| 168 | + cls.binary_site, lily_platform, lily_version, lily_platform) |
| 169 | + |
| 170 | + |
| 171 | + @classmethod |
| 172 | + def _get_cached_versions(cls): |
| 173 | + """Return a list of dictionaries with the attributes of cached |
| 174 | + versions""" |
| 175 | + versions = [] |
| 176 | + for platform in os.listdir(cls.cache_root): |
| 177 | + dname = osp.join(cls.cache_root, platform) |
| 178 | + if osp.isdir(dname): |
| 179 | + for version in os.listdir(dname): |
| 180 | + if osp.isdir(osp.join(dname, version)): |
| 181 | + cmd = LilyCmd.with_version(platform, version) |
| 182 | + versions.append( |
| 183 | + {'platform': platform, |
| 184 | + 'version': version, |
| 185 | + 'last_used': cmd._last_used(), |
| 186 | + 'directory': osp.abspath(osp.join(dname, version))}) |
| 187 | + return versions |
0 commit comments