├── .gitignore ├── .gitmodules ├── README.md ├── bin └── homedir ├── bump-version ├── cache └── 00init.sh └── lib ├── .gitignore ├── homedir.py └── homedir ├── __init__.py ├── catalog.py ├── handle.py ├── package.py ├── pathname.py └── setup.py /.gitignore: -------------------------------------------------------------------------------- 1 | /*.repo/ 2 | *~ 3 | *.tmproj 4 | -------------------------------------------------------------------------------- /.gitmodules: -------------------------------------------------------------------------------- 1 | [submodule "wiki"] 2 | path = wiki 3 | url = git@github.com:docwhat/homedir.wiki.git 4 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Welcome to HomeDir 2 | 3 | Do you have a home directory? Want to keep it sane, safe, and easy to use on multiple systems? 4 | 5 | Then you want HomeDir! 6 | 7 | ## What is homedir? 8 | 9 | It is, in essence, a package manager for your home directory. 10 | 11 | It stores *packages* in your `~/.homedir/packages` directory. These packages can be pulled from my 12 | [examples](https://github.com/docwhat/homedir-examples) or you can set up your own! 13 | 14 | This allows you to: 15 | 16 | * Version control your home directory files. Your home directory files represent lots and lots of effort. Why wouldn't you want them archived? 17 | * Share home directory files across multiple hosts. It's much nicer when all the systems you work on behave the same. 18 | 19 | ## Requirements 20 | 21 | You must have python 2.5 or newer. 22 | 23 | ## Quick Start 24 | 25 | If you've never used HomeDir, then just run this from the command line: 26 | 27 | curl -L -o- https://github.com/docwhat/homedir/raw/master/lib/homedir/setup.py | python 28 | 29 | If you don't have curl, then just download (usually right-click and then select "save as...") the file [setup.py](https://github.com/docwhat/homedir/raw/master/lib/homedir/setup.py) and run it with your copy of python. 30 | 31 | If you already have `~/bin` in your path, then homedir will "Just Work™". 32 | 33 | ### Previous HomeDir users 34 | 35 | If you've using the pre-2.0 version of HomeDir (hosted on trac.gerf.org) then you should back up your .homedir directory before running the above `setup.py` script. 36 | 37 | The main change is that everything in `~/.homedir/files` has been moved to `~/.homedir/packages` and the packages will no longer be automatically updated. 38 | 39 | ## The Story So Far… 40 | 41 | Since about 1999 I've been keeping my home directory config files in 42 | [CVS](http://www.nongnu.org/cvs/). As the number of config files I've 43 | been storing has grown and as the number of different systems I use it 44 | on (my work desktop, my home desktop, my laptop, Gerf, my pda) 45 | increases. As the complexity has grown it has become harder to 46 | maintain. 47 | 48 | So, I started looking around for a better solution. I noticed that 49 | [SVN](http://subversion.tigris.org/) is a much better version control 50 | system. I was already familiar with 51 | [stow](http://www.gnu.org/software/stow/stow.html) as well. So I 52 | tinkered around with combining them. 53 | 54 | And thus homedir was born! 55 | 56 | Since then, I rewrote HomeDir completely in python. This gives me more 57 | control over the help and error messages. I can do better when 58 | conflicts arrive. As well as I can add an elementry package format, 59 | which allows me to solve the problem that uninstalling a stow package 60 | can take forever if you have a lot of directories in your home. 61 | 62 | A [friend](http://willnorris.com/) pointed out that the most important 63 | part was the package manager. The various .dotfiles are interesting 64 | to look at, but most people have their own. So I moved just the 65 | package manager portion to [github](http://github.com/) 66 | 67 | ## Similar Ideas 68 | 69 | * I have also noticed that [Joey Hess](http://www.kitenet.net/~joey) has 70 | also done something similar and written two articles about it: 71 | * [CVS homedir, or keeping your life in CVS](http://kitenet.net/~joey/cvshome.html) 72 | * [Subversion Users: home directory in svn?](http://www.kitenet.net/~joey/svnhome.html) 73 | 74 | ## Changes since version 1 75 | 76 | The original HomeDir, which was hosted on trac.gerf.org, is going to be considered version 1. 77 | 78 | ### No Configuration File 79 | 80 | Version 1 had a configuration file that kept track of where your packages were located. 81 | 82 | In this new version packages can be placed anyplace under the directory 83 | `~/.homedir/packages` (except inside another package, of course). 84 | 85 | This allows for more flexability for managing packages. 86 | 87 | This also means that sync/synccmd is no longer supported. 88 | 89 | ## Plans/Todo 90 | 91 | * Finish moving out the package and dependency checking stuff into a Catalog class. 92 | * Fix cache-tool package in examples. 93 | * Add `homedir-pkg ...` command to make building a package from existing files easier. 94 | * Get a dedicated freenode #homedir channel: "hello freenode" to mrmist 95 | -------------------------------------------------------------------------------- /bin/homedir: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python -utWall 2 | # -*- coding: utf-8 -*- 3 | COPYRIGHT = """ 4 | This is a package management system designed to work around packages 5 | for your home directory. The code is based upon ideas from GNU Stow. 6 | 7 | HomeDir - manage your home directory. 8 | Copyright (C) 2004-2012 by Christian Höltje 9 | 10 | This program is free software; you can redistribute it and/or modify 11 | it under the terms of the GNU General Public License as published by 12 | the Free Software Foundation; either version 2 of the License, or 13 | (at your option) any later version. 14 | 15 | This program is distributed in the hope that it will be useful, but 16 | WITHOUT ANY WARRANTY; without even the implied warranty of 17 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 18 | General Public License for more details. 19 | 20 | You should have received a copy of the GNU General Public License 21 | along with this program; if not, write to the Free Software 22 | Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA. 23 | """ 24 | import os, sys, traceback 25 | 26 | sys.path.insert(0, os.path.join(os.path.dirname(os.path.dirname(os.path.realpath(__file__))), 'lib')) 27 | 28 | ## TODO - Installation tracker so that user can just 'update' without specifying packages. 29 | 30 | VERSION="2.9" 31 | COMMANDS = None # this is set below 32 | HOME = os.path.expanduser("~/") # easy way to do it 33 | HOMEDIR = os.path.expanduser("~/.homedir") 34 | 35 | ### 36 | ### Version Check 37 | ### 38 | if sys.version_info < (2,5): 39 | print >> sys.stderr, """ 40 | 41 | This program requires python 2.5. 42 | You're running python %(ver)s 43 | 44 | Please update your python. 45 | 46 | If you have a newer python on the system, then please run: 47 | /path/to/your/newer/python %(name)s setup --force 48 | 49 | It will update your copy of homedir and fix the path to python. 50 | """ % { 51 | 'ver': sys.version, 52 | 'name': os.path.basename(__file__) 53 | } 54 | sys.exit(1) 55 | 56 | import optparse 57 | from homedir.package import * 58 | from homedir.catalog import * 59 | from homedir.pathname import Pathname 60 | from homedir.setup import getch 61 | from homedir.handle import * 62 | 63 | class resolveConflict: 64 | counter = 0 65 | def __call__(package, src, dst): 66 | "Ask the user to resolve any conflicts" 67 | assert(isinstance(src, Pathname)) 68 | assert(isinstance(dst, Pathname)) 69 | if not package.counter: 70 | print "[conflict]" 71 | package.counter += 1 72 | 73 | legit_answers = ['c','d','s'] 74 | 75 | # Information about the source 76 | src_display = None 77 | if src.isdir(): 78 | src_display = "I want to replace it with a directory" 79 | elif src.exists(): 80 | src_display = "I want to replace it with a file" 81 | 82 | 83 | # Possible move candidate 84 | dstmove = Pathname("%s.bak" % dst) 85 | if dstmove.exists(): 86 | legit_answers.append('o') 87 | else: 88 | legit_answers.append('r') 89 | legit_answers.append('q') 90 | 91 | # Collect possible extra information 92 | dst_display = dst 93 | if dst.isdir(): 94 | ftype = "dir" 95 | elif dst.islink(): 96 | ftype = 'symlink' 97 | elif dst.exists(): 98 | ftype = "file" 99 | else: 100 | ftype = "????" 101 | 102 | link_display = None 103 | if dst.islink(): 104 | link_display = "It points to '%s'" % dst.readlink() 105 | if dst.exists(): 106 | ftype = "symlink" 107 | else: 108 | ftype = "broken symlink" 109 | link_display += " which does not exist" 110 | link_display += '.' 111 | 112 | answer = '' 113 | count = 0 114 | while answer not in legit_answers: 115 | count += 1 116 | if count > 20: 117 | print "...Skipping..." 118 | answer = 's' 119 | continue 120 | print " ","*"*40 121 | print " CONFLICT: The %s '%s' is in the way." % (ftype,dst_display) 122 | if link_display: 123 | print " %s" % link_display 124 | if src_display: 125 | print " %s" % src_display 126 | print " I can do the following:" 127 | print " c - cancel package" 128 | print " d - destroy the current file" 129 | print " s - skip file" 130 | if 'r' in legit_answers: 131 | print " r - rename '%s' to '%s'" % (dst,dstmove.basename()) 132 | if 'o' in legit_answers: 133 | print " o - rename '%s' to '%s', over-writing the existing .bak file" % (dst,dstmove.basename()) 134 | print " q - quit" 135 | print " Your choice? [%s] " % "/".join(legit_answers), 136 | answer = getch().strip().lower() 137 | if answer == '!': 138 | raise StandardError('You triggered a debugging feature!') 139 | 140 | print 141 | if answer == 'q': 142 | print "Okay then, quitting..." 143 | sys.exit(0) 144 | elif answer == 'c': 145 | raise ConflictError(src=src, dst=dst) 146 | elif answer == 'd': 147 | if dst.exists(): 148 | dst.unlink() 149 | return True 150 | elif answer == 's': 151 | return False 152 | elif answer == 'r': 153 | if dstmove.exists(): 154 | raise ConflictError("A file has prevented backup %s"%dstmove, 155 | src=src, dst=dst) 156 | dst.rename(dstmove) 157 | return True 158 | elif answer == 'o': 159 | if dstmove.exists(): 160 | dstmove.unlink() 161 | dst.rename(dstmove) 162 | return True 163 | else: 164 | raise AssertionError("The while loop should prevent this from ever happening.") 165 | Package.conflict_resolver = resolveConflict() 166 | 167 | def actionLoop( func, action, packages ): 168 | for package in packages: 169 | try: 170 | package.conflict_resolver.counter = 0 171 | start = "%-60s" % (" %s %s..." % (action,package.package)) 172 | print start, 173 | func(package) 174 | if package.conflict_resolver.counter: 175 | print start, 176 | print "[ok]" 177 | except ConflictError,err: 178 | print "[incomplete]" 179 | print >> sys.stderr, "There was an unresolved conflict while %s '%s' on file:" % (action,package.package) 180 | print >> sys.stderr, " %s" % err 181 | 182 | def do_list(options, catalog): 183 | "Do the list command" 184 | 185 | pkgs = catalog.all() 186 | pkgs.sort() 187 | 188 | # Get the maximum name size for better formatting. 189 | max_name = reduce(lambda prev,next: max(prev,len(next)), [p.name for p in pkgs], 0) 190 | 191 | template = " %%-%ds %%s" % max_name 192 | 193 | print template % ("name", 194 | "description") 195 | print template % ("-" * max_name, 196 | "-" * (78 - max_name - 2 )) 197 | 198 | for pkg in pkgs: 199 | print template % (pkg.package, 200 | pkg.short_description) 201 | 202 | print "%d packages" % len(pkgs) 203 | 204 | def do_install(options, catalog, *packages): 205 | "Do the install command" 206 | # lookup the packages 207 | packages = catalog.find(*packages) 208 | sorted_packages = list(packages) 209 | sorted_packages.sort() 210 | 211 | print 212 | print "You asked me to install %s:" % pluralize('this package', 213 | 'these packages', 214 | len(packages)) 215 | for p in sorted_packages: 216 | print " %s \t%s" % (p.package,p.short_description) 217 | 218 | 219 | # Do dependencies 220 | deps = catalog.findDependencies(*packages).difference(packages) 221 | sorted_deps= list(deps) 222 | sorted_deps.sort() 223 | 224 | if deps: 225 | print 226 | print "I need to install the following extra %s to meet dependencies:"%\ 227 | pluralize('package','packages',len(deps)) 228 | for p in sorted_deps: 229 | print " %s \t%s" % (p.package,p.short_description) 230 | 231 | print "Is that okay? [Y/n] ", 232 | response = sys.stdin.readline().strip() 233 | if response and response[0].upper() != 'Y': 234 | print "Okay then, quitting..." 235 | sys.exit(0) 236 | print "Installing Packages..." 237 | actionLoop( lambda p:p.install(HOME), 'installing', packages.union(deps)) 238 | 239 | def do_remove(options, catalog, *packages): 240 | "Do the uninstall command" 241 | # lookup the packages 242 | 243 | packages = catalog.find(*packages) 244 | sorted_packages = list(packages) 245 | sorted_packages.sort() 246 | 247 | print 248 | print "You asked me to remove %s:" % pluralize('this package', 249 | 'these packages', 250 | len(packages)) 251 | for p in sorted_packages: 252 | print " %s \t%s" % (p.package,p.short_description) 253 | if not packages: 254 | print " None" 255 | 256 | 257 | # Do dependencies 258 | deps = catalog.findReverseDependencies(*packages).difference(packages) 259 | sorted_deps= list(deps) 260 | sorted_deps.sort() 261 | 262 | if deps: 263 | print 264 | print "The following %s depend on the above %s and will be" % ( 265 | pluralize('package','packages',len(deps)), 266 | pluralize('package','packages',len(packages))) 267 | 268 | print "removed if installed:" 269 | for p in sorted_deps: 270 | print " %s \t%s" % (p.package, p.short_description) 271 | 272 | print "Is that okay? [y/N] ", 273 | response = sys.stdin.readline().strip() 274 | if not response or response[0].upper() == 'N': 275 | print "Okay then, quitting..." 276 | sys.exit(0) 277 | 278 | print "Removing Packages..." 279 | 280 | actionLoop( lambda p:p.remove(HOME), 'removing', deps.union(packages) ) 281 | 282 | def do_upgrade(options, catalog, *packages): 283 | 284 | packages = catalog.find(*packages) 285 | sorted_packages = list(packages) 286 | sorted_packages.sort() 287 | 288 | print 289 | print "You asked me to upgrade %s:" % pluralize('this package', 290 | 'these packages', 291 | len(packages)) 292 | 293 | for p in sorted_packages: 294 | print " %s \t%s" % (p.package,p.short_description) 295 | if not packages: 296 | print " None" 297 | 298 | # Do dependencies 299 | deps = catalog.findDependencies(*packages).difference(packages) 300 | sorted_deps= list(deps) 301 | sorted_deps.sort() 302 | 303 | # don't duplicate packages, please! 304 | rdeps = catalog.findReverseDependencies(*packages).difference(packages).difference(deps) 305 | sorted_rdeps= list(rdeps) 306 | sorted_rdeps.sort() 307 | 308 | 309 | if deps: 310 | print 311 | print "The following %s have dependencies that interact with " %\ 312 | pluralize('package','packages',len(deps)) 313 | print "the above %s and will be upgraded as well:" % pluralize( 314 | 'package','packages',len(deps)) 315 | for p in sorted_deps: 316 | print " %s \t%s" % (p.package,p.short_description) 317 | 318 | if rdeps: 319 | print 320 | print "You might need to re-install %s" % \ 321 | pluralize('this package','these packages',len(rdeps)), 322 | print "manually afterwards:" 323 | for p in sorted_rdeps: 324 | print " %s \t%s" % (p.package,p.short_description) 325 | 326 | if deps or rdeps: 327 | print 328 | print "Is that okay? [y/N] ", 329 | response = sys.stdin.readline().strip() 330 | if not response or response[0].upper() == 'N': 331 | print "Okay then, quitting..." 332 | sys.exit(0) 333 | 334 | print "Updating Packages..." 335 | 336 | # UnInstall Only 337 | actionLoop( lambda p:p.remove(HOME), 'removing', rdeps ) 338 | def func(package): 339 | package.remove(HOME) 340 | package.install(HOME) 341 | 342 | actionLoop( func, 'upgrading', packages.union(deps) ) 343 | 344 | def do_setup(options, catalog): 345 | from homedir.setup import Setup, getVersion 346 | if options.force: 347 | latest = VERSION + 'nomatch' 348 | else: 349 | latest = getVersion() 350 | if latest == VERSION: 351 | print "You already have the latest version." 352 | else: 353 | Setup(via_web=True) 354 | 355 | COMMANDS = {'list': do_list, 356 | 'install': do_install, 357 | 'remove': do_remove, 358 | 'upgrade': do_upgrade, 359 | 'setup': do_setup, 360 | } 361 | 362 | class Application: 363 | "A Class to hold the core application functions." 364 | 365 | 366 | def doParse(): 367 | """Actually does the command line parsing. 368 | Returns (options, args) (as per optparse's parse_args 369 | """ 370 | usage = """%prog [options] arg(s) 371 | 372 | commands: 373 | install PKG ... Install a package. 374 | remove PKG ... Uninstall a package. 375 | upgrade PKG ... Upgrade your installed packages (reinstall). 376 | list List all packages. 377 | setup [-f|--force] Install or upgrade homedir (force even if up-to-date).""" 378 | parser = optparse.OptionParser(version=VERSION, usage=usage) 379 | 380 | parser.add_option('-q','--quiet', 381 | action="store_true", dest="quiet", 382 | default=False, 383 | help="Run without warnings and messages. Errors are still shown.") 384 | ## parser.add_option('-n','--dry-run', 385 | ## action="store_true", dest="dry_run", 386 | ## default=False, 387 | ## help="Show the actions that would have been taken, " 388 | ## "but don't actually do them") 389 | parser.add_option('-f', '--force', action="store_true", help=optparse.SUPPRESS_HELP) 390 | parser.add_option('-d','--debug', 391 | action="store_true", dest="debug", 392 | default=False, 393 | help=optparse.SUPPRESS_HELP) 394 | parser.add_option('--copyright', action="store_true", help="Show the copyright and exit.") 395 | 396 | (options,args) = parser.parse_args() 397 | 398 | warn_mode(options.debug) 399 | 400 | if options.copyright: 401 | print COPYRIGHT 402 | sys.exit(0) 403 | 404 | if not args: 405 | parser.print_help() 406 | sys.exit(0) 407 | if args[0] not in COMMANDS.keys(): 408 | parser.error("Invalid command '%s'" % args[0]) 409 | 410 | return options,args 411 | 412 | if "__main__" == __name__: 413 | try: 414 | options, args = doParse() 415 | command = args[0] 416 | rest_args = args[1:] 417 | catalog = Catalog(debug=options.debug) 418 | 419 | COMMANDS[command](options,catalog,*rest_args) 420 | except KeyboardInterrupt: 421 | print >> sys.stderr, "\nUser Aborted", 422 | sys.exit(1) 423 | 424 | 425 | # Local Variables: 426 | # mode: python 427 | # tab-width: 4 428 | # indent-tabs-mode: nil 429 | # End: 430 | # vim: set sw=4 ts=4 expandtab 431 | -------------------------------------------------------------------------------- /bump-version: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python -utWall 2 | 3 | import os, sys 4 | from subprocess import * 5 | 6 | sys.path.insert(0, os.path.join(os.path.dirname(os.path.realpath(__file__)), 'lib')) 7 | 8 | from homedir.pathname import * 9 | 10 | homedir = Pathname(__file__).dirname() + 'bin' + 'homedir' 11 | 12 | data = [] 13 | major, minor, old_minor = None, None, None 14 | f = homedir.open('r') 15 | try: 16 | for line in f.readlines(): 17 | line = line.rstrip() 18 | if line.startswith('VERSION="'): 19 | ver = line[len('VERSION="'):-1] 20 | major, minor = [int(x) for x in ver.split('.')] 21 | old_minor = minor 22 | minor = minor + 1 23 | line = 'VERSION="%d.%d"' % (major, minor) 24 | data.append(line) 25 | finally: 26 | f.close() 27 | 28 | if major is None or minor is None or old_minor is None: 29 | raise StandardError("Failed to parse version.") 30 | 31 | f = homedir.open('w') 32 | try: 33 | for line in data: 34 | f.write(line) 35 | f.write("\n") 36 | finally: 37 | f.close() 38 | 39 | print "Committing..." 40 | output = Popen(["git", "commit", '-m', 'Bumped version to %d.%d' % (major, minor), unicode(homedir)], stdout=PIPE).communicate()[0] 41 | print output 42 | 43 | print "Tagging..." 44 | tagname = "version-%d.%d" % (major, minor) 45 | output = Popen(["git", "tag", '-m', 'Bumped version to %d.%d' % (major, minor), tagname], stdout=PIPE).communicate()[0] 46 | print output 47 | 48 | print "Done!" 49 | 50 | # EOF 51 | -------------------------------------------------------------------------------- /cache/00init.sh: -------------------------------------------------------------------------------- 1 | ## Sets up an environmental variable for the cache directory 2 | export CACHE_BASE_DIR="${HOME}/.homedir/cache" 3 | set -eu 4 | 5 | function getHomedirCache() { 6 | local PROGRAM="$1" 7 | shift 8 | local DIR="${CACHE_BASE_DIR}/${PROGRAM}" 9 | if [ ! -d "${DIR}" ]; then 10 | mkdir "${DIR}" 11 | fi 12 | echo "${DIR}" 13 | } 14 | 15 | # Fetches a file 16 | function homedirFetcher() { 17 | local URL="$1" 18 | shift 19 | local OUTFILE="$1" 20 | shift 21 | local AGE="$1" 22 | shift 23 | 24 | local md5sum='none' 25 | local doFetch 26 | if [ -e "${OUTFILE}" ]; then 27 | doFetch=$(find "${OUTFILE}" -ctime +${AGE} -print) 28 | if [ -n "${doFetch}" ]; then 29 | md5sum=$(md5sum "${OUTFILE}") 30 | fi 31 | else 32 | doFetch=1 33 | fi 34 | 35 | if [ -n "${doFetch}" ]; then 36 | wget -q -O "${OUTFILE}" "${URL}" 37 | #curl -o "${OUTFILE}" "${URL}" 38 | #lynx -source "${URL}" > "${OUTFILE}" 39 | local newmd5sum 40 | newmd5sum=$(md5sum "${OUTFILE}") 41 | if [ "${md5sum}" = "${newmd5sum}" ]; then 42 | echo "cached" 43 | else 44 | echo "fetched" 45 | fi 46 | else 47 | echo "cached" 48 | fi 49 | } 50 | 51 | #EOF 52 | -------------------------------------------------------------------------------- /lib/.gitignore: -------------------------------------------------------------------------------- 1 | *.pyc 2 | -------------------------------------------------------------------------------- /lib/homedir.py: -------------------------------------------------------------------------------- 1 | ../bin/homedir -------------------------------------------------------------------------------- /lib/homedir/__init__.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | """ 3 | This is a package management system designed to work around packages 4 | for the homedirectory. The code is based upon ideas from GNU Stow. 5 | 6 | HomeDir - manage the installation of packages for a user's homedir 7 | Copyright (C) 2004-2012 by Christian Höltje 8 | 9 | This program is free software; you can redistribute it and/or modify 10 | it under the terms of the GNU General Public License as published by 11 | the Free Software Foundation; either version 2 of the License, or 12 | (at your option) any later version. 13 | 14 | This program is distributed in the hope that it will be useful, but 15 | WITHOUT ANY WARRANTY; without even the implied warranty of 16 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 17 | General Public License for more details. 18 | 19 | You should have received a copy of the GNU General Public License 20 | along with this program; if not, write to the Free Software 21 | Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA. 22 | """ 23 | 24 | __all__ = ( 'package', 'setup', 'catalog', 'handle', 'pathname' ) 25 | 26 | # vim: set sw=4 ts=4 expandtab 27 | -------------------------------------------------------------------------------- /lib/homedir/catalog.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | """ 3 | This is a package management system designed to work around packages 4 | for the homedirectory. The code is based upon ideas from GNU Stow. 5 | 6 | HomeDir - manage the installation of packages for a user's homedir 7 | Copyright (C) 2004-2012 by Christian Höltje 8 | 9 | This program is free software; you can redistribute it and/or modify 10 | it under the terms of the GNU General Public License as published by 11 | the Free Software Foundation; either version 2 of the License, or 12 | (at your option) any later version. 13 | 14 | This program is distributed in the hope that it will be useful, but 15 | WITHOUT ANY WARRANTY; without even the implied warranty of 16 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 17 | General Public License for more details. 18 | 19 | You should have received a copy of the GNU General Public License 20 | along with this program; if not, write to the Free Software 21 | Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA. 22 | """ 23 | 24 | import os, sys 25 | from package import * 26 | 27 | class MissingPackageError(StandardError): pass 28 | 29 | class Catalog: 30 | ## 31 | # Arguments: 32 | # * debug -- Turn on debugging in the catalog code. 33 | # * mock_packages -- A hash of mock packages for testing purposes. 34 | def __init__(self, debug=False, mock_packages=None): 35 | self.packages = packages = {} 36 | self.debug = debug 37 | if mock_packages is not None: 38 | self.packages = mock_packages 39 | else: 40 | top = os.path.expanduser("~/.homedir/packages") 41 | 42 | ## Gather the packages. 43 | def walker(arg, dirname, fnames): 44 | try: 45 | package = Package(dirname, self) 46 | packages[package.package] = package 47 | while len(fnames) > 0: 48 | del fnames[0] 49 | except NotPackageError,err: 50 | for i in range(len(fnames)-1, 0-1, -1): 51 | fname = fnames[i] 52 | if fname.startswith('.'): 53 | del fnames[i] 54 | 55 | os.path.walk(top, walker, None) 56 | self.packages = packages 57 | 58 | def findOne(self, name): 59 | "Returns one package or None" 60 | res = self.find(name) 61 | if res: 62 | return tuple(res)[0] 63 | else: 64 | return None 65 | 66 | def find(self, *names): 67 | """Finds a package based on name. 68 | 69 | Returns a list of packages. 70 | """ 71 | packages = set() 72 | bad = set() 73 | 74 | for name in names: 75 | if isinstance(name,Package) and \ 76 | self.packages.has_key(name.package): 77 | packages.add(name) 78 | elif self.packages.has_key(name): 79 | packages.add(self.packages[name]) 80 | else: 81 | bad.add(name) 82 | if bad: 83 | raise MissingPackageError("Unknown packages: %s" % (",".join(map(str,bad)))) 84 | return packages 85 | 86 | 87 | def all(self): 88 | "Returns all packages" 89 | return self.packages.values() 90 | 91 | def findDependencies(self, *packages, **kwargs): 92 | "Returns all dependencies for the list of packages." 93 | 94 | found = kwargs.get('found', set()) 95 | packages = set([self.packages.get(x,x) for x in packages]) 96 | 97 | for package in packages: 98 | deps = package.depends 99 | for dep in deps: 100 | found.add(dep) 101 | self.findDependencies(dep, found=found) 102 | 103 | return found 104 | 105 | def findReverseDependencies(self, *packages, **kwargs): 106 | """ 107 | Returns all reverse dependencies for the list of packages. 108 | """ 109 | 110 | found = kwargs.get('found', set()) 111 | packages = set([self.packages.get(x,x) for x in packages]) 112 | 113 | for parent in self.packages.values(): 114 | for dependent in packages: 115 | if dependent in parent.depends: 116 | found.add(parent) 117 | # Add all parent packages too. 118 | self.findReverseDependencies(parent, found=found) 119 | 120 | return found 121 | 122 | if __name__ == "__main__": 123 | import unittest 124 | 125 | class TestCatalog(unittest.TestCase): 126 | class MockPackage: 127 | def __init__(self, name, depends_on=None): 128 | if depends_on is None: 129 | depends_on = [] 130 | self.depends = depends_on 131 | self.name = name 132 | def __repr__(self): 133 | return "" % self.name 134 | 135 | def test_findDependencies(self): 136 | # Setup 137 | mock_grandchild = self.MockPackage("grandchild") 138 | mock_dependent = self.MockPackage("dependent", depends_on=[mock_grandchild]) 139 | mock_parent = self.MockPackage("parent", depends_on=[mock_dependent]) 140 | mock_packages = {'parent': mock_parent, 141 | 'dependent': mock_dependent, 142 | 'grandchild': mock_grandchild, 143 | } 144 | catalog = Catalog(debug=True, mock_packages=mock_packages) 145 | 146 | # Activity 147 | dependents = catalog.findDependencies(mock_parent) 148 | 149 | # Verify 150 | expected = set([mock_dependent, mock_grandchild]) 151 | self.assertEqual(expected, dependents) 152 | 153 | def test_findReverseDependencies(self): 154 | # Setup 155 | mock_dependent = self.MockPackage("dependent") 156 | mock_parent = self.MockPackage("parent", depends_on=[mock_dependent]) 157 | mock_grandparent = self.MockPackage("grandparent", depends_on=[mock_parent]) 158 | mock_packages = { 159 | 'parent': mock_parent, 160 | 'dependent': mock_dependent, 161 | 'grandparent': mock_grandparent, 162 | } 163 | catalog = Catalog(debug=True, mock_packages=mock_packages) 164 | 165 | # Activity 166 | parents = catalog.findReverseDependencies(mock_dependent) 167 | 168 | # Verify 169 | expected = set([mock_parent, mock_grandparent]) 170 | self.assertEqual(expected, parents, "Expected %r items, got %r" % (expected, parents)) 171 | 172 | unittest.main() 173 | # cat = Catalog() 174 | # pkgs = cat.packages.values() 175 | # pkgs.sort() 176 | # for pkg in pkgs: 177 | # print "------------------------" 178 | # #pkg.prettyPrint() 179 | # print pkg.name 180 | # print cat.findDependencies(pkg) 181 | # 182 | # EOF 183 | -------------------------------------------------------------------------------- /lib/homedir/handle.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | """ 3 | This is a package management system designed to work around packages 4 | for the homedirectory. The code is based upon ideas from GNU Stow. 5 | 6 | HomeDir - manage the installation of packages for a user's homedir 7 | Copyright (C) 2004-2012 by Christian Höltje 8 | 9 | This program is free software; you can redistribute it and/or modify 10 | it under the terms of the GNU General Public License as published by 11 | the Free Software Foundation; either version 2 of the License, or 12 | (at your option) any later version. 13 | 14 | This program is distributed in the hope that it will be useful, but 15 | WITHOUT ANY WARRANTY; without even the implied warranty of 16 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 17 | General Public License for more details. 18 | 19 | You should have received a copy of the GNU General Public License 20 | along with this program; if not, write to the Free Software 21 | Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA. 22 | """ 23 | 24 | __all__ = ( 'warn_mode', 'warn', 'pluralize' ) 25 | 26 | # Warn mode helper 27 | WARN = False 28 | 29 | def warn_mode(mode): 30 | "Set the mode to true or false." 31 | WARN = bool(mode) 32 | 33 | def warn(*msg): 34 | "Either prints a warning message or is a nop, depending on options" 35 | if WARN: 36 | print "WARN: %s" % " ".join(map(str,msg)) 37 | 38 | def pluralize(singular,plural,count): 39 | "Returns the correct form of a word, based on count" 40 | if count == 1: 41 | return singular 42 | elif count > 1 or count == 0: 43 | return plural 44 | else: 45 | raise AssertionError("Unable to pluralize") 46 | 47 | -------------------------------------------------------------------------------- /lib/homedir/package.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | """ 3 | This is a package management system designed to work around packages 4 | for the homedirectory. The code is based upon ideas from GNU Stow. 5 | 6 | HomeDir - manage the installation of packages for a user's homedir 7 | Copyright (C) 2004-2012 by Christian Höltje 8 | 9 | This program is free software; you can redistribute it and/or modify 10 | it under the terms of the GNU General Public License as published by 11 | the Free Software Foundation; either version 2 of the License, or 12 | (at your option) any later version. 13 | 14 | This program is distributed in the hope that it will be useful, but 15 | WITHOUT ANY WARRANTY; without even the implied warranty of 16 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 17 | General Public License for more details. 18 | 19 | You should have received a copy of the GNU General Public License 20 | along with this program; if not, write to the Free Software 21 | Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA. 22 | """ 23 | 24 | ## TODO: unmergeSubDir -- turn directories back to symlinks or delete. 25 | 26 | import os, sys, traceback 27 | from handle import warn 28 | from pathname import Pathname 29 | 30 | __all__ = ('NotPackageError', 'ConflictError', 'Package', 31 | 'CONTROLDIR', 'CONTROLFILENAME', 'OLD_CONTROLFILENAME', 'PKG_VERSION' ) 32 | 33 | CONTROLDIR = ".homedir" 34 | CONTROLFILENAME = "control" 35 | OLD_CONTROLFILENAME = ".homedir.control" 36 | PKG_VERSION = 1 37 | IGNORE_DIRS=('.svn','CVS','RCS','.git') 38 | 39 | # Error Classes 40 | class NotPackageError(StandardError): 41 | "This is not a package." 42 | class ConflictError(StandardError): 43 | "There has been a conflict with another package." 44 | src = None 45 | dst = None 46 | def __init__(self,src,dst,*args): 47 | self.src = src 48 | self.dst = dst 49 | StandardError.__init__(self,*args) 50 | 51 | def __str__(self): 52 | src = self.src 53 | dst = self.dst 54 | return "The file %(dst)s prevents linking %(src)s" % locals() 55 | 56 | class Package(object): 57 | """HomeDir Package class. 58 | 59 | """ 60 | package = None 61 | priority = None 62 | maintainer = None 63 | standards_version = None 64 | description = None 65 | dirs = None 66 | mkdirs = None 67 | 68 | package_location = None 69 | src_dirs = None 70 | src_mkdirs = None 71 | 72 | conflict_resolver = None 73 | 74 | _attributes = ('package','priority','maintainer','depends', 75 | 'standards-version','description','dirs','mkdirs', 76 | 'ubuntu-packages') 77 | 78 | @apply 79 | def name(): 80 | def fget(self): 81 | return self.package 82 | return property(**locals()) 83 | 84 | def __init__(self, directory, catalog): 85 | self.catalog = catalog 86 | self.package_location = directory = Pathname(directory).realpath() 87 | self._depends = set() 88 | 89 | # Find the control directory, supporting the old name. 90 | control = directory + CONTROLDIR + CONTROLFILENAME 91 | if not control.isfile(): 92 | control = directory + OLD_CONTROLFILENAME 93 | 94 | if not control.isfile(): 95 | raise NotPackageError("No control file") 96 | 97 | self._parse(control) 98 | 99 | def __repr__(self): 100 | return "<%s %s>" % (self.__class__.__name__, 101 | self.package) 102 | 103 | def __eq__(self,other): 104 | if isinstance(other,self.__class__): 105 | return self.package_location == other.package_location 106 | elif isinstance(other,str): 107 | return self.package == other 108 | else: 109 | raise TypeError( "%s cannot be compared to %s" % ( 110 | self.__class__, type(other))) 111 | 112 | @apply 113 | def depends(): 114 | def fget(self): 115 | return self.catalog.find(*self._depends) 116 | return property(**locals()) 117 | 118 | def _parse(self,control): 119 | curr = None 120 | fp = file(unicode(control),'r') 121 | num = 0 122 | for line in fp.readlines(): 123 | num += 1 124 | line = line.rstrip() 125 | sline = line.strip() 126 | # Empty line, interrupts a list 127 | if sline == '': 128 | curr = None 129 | continue 130 | # Comments are ignored 131 | if sline.startswith('#') or sline.startswith(';'): 132 | continue 133 | if curr and \ 134 | ( line.startswith(' ') or line.startswith('\t') ): 135 | self._attribute_append(curr,line,control,num) 136 | continue 137 | 138 | try: 139 | # Try to see if it's an attribute 140 | parts = line.split(':',1) 141 | if len(parts) > 1: 142 | attribute,value = parts 143 | value.rstrip() 144 | else: 145 | attribute = parts[0] 146 | value = None 147 | except ValueError: 148 | print >> sys.stderr, "Invalid control file: %s:%d" % (control,num) 149 | sys.exit(1) 150 | 151 | if attribute not in self._attributes: 152 | print >> sys.stderr, "Invalid attribute '%s' in control file:\n\t%s:%d" % ( 153 | attribute,control,num) 154 | sys.exit(1) 155 | self._attribute_set(attribute,value,control,num) 156 | curr = attribute 157 | 158 | # Validate the mkdirs -- must be in dirs 159 | if self.mkdirs and self.dirs: 160 | for mkdir in self.mkdirs: 161 | if mkdir not in self.dirs: 162 | print >> sys.stderr, \ 163 | "Invalid mkdir: '%s' isn't marked as a dir" % \ 164 | mkdir 165 | sys.exit(1) 166 | 167 | # List of real locations for the dirs in src 168 | src_dirs = self.src_dirs = [] 169 | if self.dirs: 170 | for directory in self.dirs: 171 | src_dirs.append(self.package_location + directory) 172 | 173 | # List of real locations of diretories to make in src 174 | src_mkdirs = self.src_mkdirs = [] 175 | if self.mkdirs: 176 | for mkdir in self.mkdirs: 177 | src_mkdirs.append(self.package_location + mkdir) 178 | 179 | def _attribute_set(self,attr,val,file,linenum): 180 | "Internal Method to set an attribute" 181 | if attr in ('mkdirs','dirs'): 182 | if val.strip(): 183 | print >> sys.stderr, "%s start on the next line: %s:%d" % ( 184 | attr, file,linenum) 185 | sys.exit(1) 186 | setattr(self,attr, []) 187 | elif attr == "depends": 188 | self._depends = set([x.strip() for x in val.split(',') if x]) 189 | elif attr == 'standards-version': 190 | val = int(val) 191 | if val != PKG_VERSION: 192 | raise NotPackageError("Invalid control file version: %s" % file) 193 | self.standard_version = val 194 | elif attr in self._attributes: 195 | setattr( self, attr, val.strip() ) 196 | else: 197 | raise AssertionError("Invalid Attribute %s: %s:%d" % (attr,file,linenum)) 198 | 199 | def _attribute_append(self,attr,val,file,linenum): 200 | "Internal Method to correct append to an attribute" 201 | if attr in ('mkdirs','dirs'): 202 | getattr(self,attr).append(val.strip()) 203 | elif attr == "depends": 204 | self._depends.update(set([x.strip() for x in val.split(',')])) 205 | elif attr == 'standards-version': 206 | raise AssertionError("Can't append %s" % attr) 207 | elif attr in self._attributes: 208 | setattr(self, attr, getattr(self,attr) + '\n' + val.rstrip()) 209 | else: 210 | raise AssertionError("Invalid Attribute %s: %s:%d" % (attr,file,linenum)) 211 | 212 | def _resolveConflict(self,src,dst): 213 | if self.conflict_resolver: 214 | return self.conflict_resolver(src,dst) 215 | else: 216 | raise ConflictError(src=src, dst=dst) 217 | 218 | def normalize(self, catalog): 219 | self.depends = catalog.find(*self.depends) 220 | 221 | def prettyPrint(self): 222 | "Pretty Print the package" 223 | def strify(o): 224 | if o is None: 225 | return 'none' 226 | if isinstance(o, self.__class__): 227 | return o.package 228 | if isinstance(o, list): 229 | return ', '.join([strify(x) for x in o]) 230 | return unicode(o) 231 | print self.package 232 | print " priority: %s" % self.priority 233 | print " maintainer: %s" % self.maintainer 234 | print " standards-version: %s" % self.standards_version 235 | print " description: %s" % self.description.split('\n')[0] 236 | print " dirs: %s" % strify(self.dirs) 237 | print " mkdirs: %s" % strify(self.mkdirs) 238 | print " package-location: %s" % strify(self.package_location) 239 | #print " src-dirs: %s" % strify(self.src_dirs) 240 | #print " src-mkdirs: %s" % strify(self.src_mkdirs) 241 | print " depends: %s" % strify(self.depends) 242 | 243 | def unsymlink(self,file): 244 | "Helper method to remove a symlink and only symlinks" 245 | assert(isinstance(file,Pathname)) 246 | if file.islink(): 247 | file.unlink() 248 | elif file.exists(): 249 | raise AssertionError("%s is not a symlink" % file) 250 | # else: It must not exist! 251 | 252 | def symlink(self, src, dst): 253 | "Perform a relative symlink" 254 | 255 | assert(isinstance(src, Pathname)) 256 | assert(isinstance(dst, Pathname)) 257 | src.relative_path_from(dst.dirname()).symlink(dst) 258 | 259 | def short_description(): 260 | doc = "Just the first line of the description" 261 | def fget(self): 262 | desc = self.description 263 | if desc: 264 | return desc.split('\n')[0] 265 | else: 266 | return "No Description" 267 | fset = fdel = None 268 | return locals() 269 | short_description = property(**short_description()) 270 | 271 | def fromSubdir(cls, directory, catalog): 272 | "Classmethod: Create a package from a subdirectory" 273 | directory = Pathname(directory) 274 | if (directory + CONTROLDIR + CONTROLFILENAME).exists() or \ 275 | (directory + OLD_CONTROLFILENAME).exists(): 276 | return cls(directory, catalog) 277 | updir = directory.dirname() 278 | if updir != os.sep: 279 | return cls.fromSubdir(updir, catalog) 280 | else: 281 | return None 282 | fromSubdir = classmethod(fromSubdir) 283 | 284 | 285 | def merge(self,dest,src=None): 286 | "Merge the package into dest" 287 | ignore_control = src is None 288 | if src is None: 289 | src = self.package_location 290 | assert(isinstance(src, Pathname)) 291 | assert(isinstance(dest, Pathname)) 292 | dest = dest.realpath() 293 | for content in src.listdir(): 294 | if content in IGNORE_DIRS: 295 | continue 296 | if ignore_control and (content == CONTROLDIR or content == OLD_CONTROLFILENAME): 297 | continue 298 | if (src + content).isdir(): 299 | self.mergeSubDir(src,dest,content) 300 | else: 301 | self.mergeNonDir(src,dest,content) 302 | 303 | def isWithinLocation(self, path): 304 | "Returns true if path is within our package location" 305 | assert(isinstance(path, Pathname)) 306 | return path.is_subdir_of(self.package_location) 307 | 308 | def mergeSubDir(self,src,dest,content): 309 | "Merge the subdirectory content from src to dest" 310 | assert(isinstance(src, Pathname)) 311 | assert(isinstance(dest, Pathname)) 312 | destpath = dest + content 313 | srcpath = src + content 314 | if srcpath not in self.src_dirs: 315 | return # We skip stuff not in directories 316 | if srcpath in self.src_mkdirs and not destpath.exists(): 317 | destpath.mkdir() 318 | if destpath.islink(): 319 | linkpath = destpath.realpath() 320 | if self.isWithinLocation(linkpath): 321 | # This is fine. The link is actually one of ours. 322 | # Nuke it to make sure it's correct 323 | self.unsymlink(destpath) 324 | self.symlink(srcpath,destpath) 325 | elif destpath.exists(): 326 | if linkpath == srcpath: 327 | warn( "%s already points to %s" % (destpath, 328 | srcpath) ) 329 | return 330 | if srcpath.isdir(): 331 | other = self.__class__.fromSubdir(linkpath, self.catalog) 332 | if not other: 333 | if self._resolveConflict(src=srcpath, 334 | dst=destpath): 335 | # Retry after the resolve 336 | self.mergeSubDir(src,dest,content) 337 | else: 338 | self.unsymlink(destpath) 339 | destpath.mkdir() 340 | self.merge(src=srcpath,dest=destpath) 341 | other.merge(src=linkpath,dest=destpath) 342 | else: 343 | raise AssertionError("Untested path") 344 | self._resolveConflict(src=srcpath, dst=destpath) 345 | else: 346 | if self._resolveConflict(src=srcpath, dst=destpath): 347 | self.unsymlink(destpath) 348 | self.symlink(srcpath,destpath) 349 | elif destpath.exists(): 350 | if destpath.isdir(): 351 | self.merge(src=srcpath,dest=destpath) 352 | else: 353 | if self._resolveConflict(src=srcpath, dst=destpath): 354 | self.symlink(srcpath,destpath) 355 | # else keep on trucking. 356 | else: 357 | self.symlink(srcpath,destpath) 358 | 359 | 360 | def mergeNonDir(self, src, dest, content): 361 | assert(isinstance(src, Pathname)) 362 | assert(isinstance(dest, Pathname)) 363 | 364 | # src is the stow directory we're merging from 365 | srcpath = src + content 366 | # dest is the target directory that we are dropping 367 | # symlinks into 368 | destpath = dest + content 369 | 370 | if destpath.islink(): 371 | linkpath = destpath.realpath() 372 | if linkpath.exists(): 373 | if self.isWithinLocation(linkpath): 374 | warn( "%s already points to %s" % (destpath, 375 | srcpath) ) 376 | else: 377 | if self._resolveConflict(src=srcpath, 378 | dst=destpath): 379 | self.symlink(srcpath,destpath) 380 | else: 381 | # It's a broken symlink and safe to nuke it (yes?) 382 | self.unsymlink(destpath) 383 | self.symlink(srcpath,destpath) 384 | elif destpath.exists(): 385 | if self._resolveConflict(src=srcpath, dst=destpath): 386 | self.symlink(srcpath,destpath) 387 | # otherwise, we're skipping the conflict 388 | else: 389 | self.symlink(srcpath,destpath) 390 | 391 | def unmerge(self,dest,only_dirs=None): 392 | "Unmerge the package from dest" 393 | 394 | assert(isinstance(dest,Pathname)) 395 | dest = dest.realpath() 396 | 397 | # We only check these dirs 398 | if only_dirs is None: 399 | only_dirs = [dest] 400 | if self.dirs: 401 | for directory in self.dirs: 402 | only_dirs.append(dest + directory) 403 | 404 | elif dest not in only_dirs: 405 | # It's not prunable, don't worry about it. 406 | return False 407 | 408 | if dest == self.package_location: 409 | return False # It's not empty 410 | 411 | is_empty = True 412 | for content in dest.listdir(): 413 | destpath = dest + content 414 | if destpath.islink(): 415 | linktarget = destpath.realpath() 416 | if self.isWithinLocation(linktarget): 417 | self.unsymlink(destpath) 418 | else: 419 | is_empty = False 420 | elif destpath.isdir(): 421 | is_destpath_empty = self.unmerge(destpath, only_dirs) 422 | is_empty = is_destpath_empty and is_empty 423 | else: 424 | is_empty = False 425 | 426 | if is_empty: 427 | try: 428 | dest.rmdir() 429 | except: 430 | tb = traceback.format_exception( *sys.exc_info() ) 431 | print >> sys.stderr, "Unable to remove directory %s:\n %s" % ( 432 | dest,tb[-1].rstrip()) 433 | 434 | return is_empty 435 | 436 | def install(self,dest,src=None): 437 | "Install the package" 438 | _src = src 439 | if _src is None: 440 | _src = self.package_location 441 | else: 442 | _src = Pathname(_src) 443 | dest = Pathname(dest) 444 | preflight = _src + CONTROLDIR + 'pre-install' 445 | if preflight.access(os.X_OK): 446 | os.system(str(preflight)) 447 | 448 | self.merge(dest,src) 449 | 450 | postflight = _src + CONTROLDIR + 'post-install' 451 | if postflight.access(os.X_OK): 452 | os.system(str(postflight)) 453 | 454 | def remove(self,dest,src=None): 455 | "Remove the package" 456 | _src = src 457 | if _src is None: 458 | _src = self.package_location 459 | else: 460 | _src = Pathname(_src) 461 | dest = Pathname(dest) 462 | 463 | preflight = _src + CONTROLDIR + 'pre-remove' 464 | if preflight.access(os.X_OK): 465 | os.system(str(preflight)) 466 | 467 | self.unmerge(dest,src) 468 | 469 | postflight = _src + CONTROLDIR + 'post-remove' 470 | if postflight.access(os.X_OK): 471 | os.system(str(postflight)) 472 | 473 | # vim: set sw=4 ts=4 expandtab 474 | -------------------------------------------------------------------------------- /lib/homedir/pathname.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | """ 3 | This is a package management system designed to work around packages 4 | for the homedirectory. The code is based upon ideas from GNU Stow. 5 | 6 | HomeDir - manage the installation of packages for a user's homedir 7 | Copyright (C) 2004-2012 by Christian Höltje 8 | 9 | This program is free software; you can redistribute it and/or modify 10 | it under the terms of the GNU General Public License as published by 11 | the Free Software Foundation; either version 2 of the License, or 12 | (at your option) any later version. 13 | 14 | This program is distributed in the hope that it will be useful, but 15 | WITHOUT ANY WARRANTY; without even the implied warranty of 16 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 17 | General Public License for more details. 18 | 19 | You should have received a copy of the GNU General Public License 20 | along with this program; if not, write to the Free Software 21 | Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA. 22 | """ 23 | 24 | import os, shutil 25 | 26 | class Pathname: 27 | """ 28 | A class to wrap a filesystem path object. This makes working with lots of paths easier. 29 | 30 | Based on 31 | """ 32 | def __init__(self, *path): 33 | path = [unicode(x) for x in path] 34 | self._path = os.path.normcase(unicode(os.path.join(*path)).rstrip(os.path.sep).rstrip(os.path.altsep)) 35 | 36 | def __str__(self): 37 | return self._path 38 | 39 | def __repr__(self): 40 | return u'<%s id="%s" path="%s">' % (self.__class__.__name__, id(self), self._path) 41 | 42 | def __add__(self, other): 43 | return Pathname(os.path.join(self._path, unicode(other))) 44 | 45 | def __eq__(self, other): 46 | return self._path == os.path.normcase(unicode(other)) 47 | 48 | def basename(self): 49 | return Pathname(os.path.basename(self._path)) 50 | 51 | def dirname(self): 52 | return Pathname(os.path.dirname(self._path)) 53 | 54 | def realpath(self): 55 | return Pathname(os.path.realpath(self._path)) 56 | 57 | def normalize(self): 58 | "expands variables, deals with ~/, fixes case, and removes double slashes" 59 | return Pathname( 60 | os.path.expanduser( 61 | os.path.expandvars( 62 | os.path.normpath( 63 | os.path.normcase( 64 | self._path 65 | ))))) 66 | 67 | def exists(self): 68 | return os.path.exists(self._path) 69 | 70 | def isfile(self): 71 | return os.path.isfile(self._path) 72 | 73 | def isdir(self): 74 | return os.path.isdir(self._path) 75 | 76 | def islink(self): 77 | return os.path.islink(self._path) 78 | 79 | def isabs(self): 80 | return os.path.isabs(self._path) 81 | 82 | def access(self, *args): 83 | return os.access(self._path, *args) 84 | 85 | def listdir(self): 86 | return tuple([Pathname(x) for x in os.listdir(self._path)]) 87 | 88 | def readlink(self): 89 | return Pathname(os.readlink(self._path)) 90 | 91 | def mkdir(self): 92 | return os.mkdir(self._path) 93 | 94 | def rmdir(self): 95 | return os.rmdir(self._path) 96 | 97 | def rm_rf(self, ignore_errors=False): 98 | return shutil.rmtree(self._path, ignore_errors) 99 | 100 | def unlink(self): 101 | return os.unlink(self._path) 102 | 103 | def open(self, *args, **kwargs): 104 | return file(self._path, *args, **kwargs) 105 | 106 | def symlink(self, dest): 107 | "Symlink the current Pathname to the dest." 108 | return os.symlink(self._path, unicode(dest)) 109 | 110 | def split(self): 111 | head, tail = os.path.split(self._path) 112 | return (Pathname(head), Pathname(tail)) 113 | 114 | def rename(self, dest): 115 | return os.rename(self._path, unicode(dest)) 116 | 117 | def relative_path_from(self, base): 118 | """Returns the a path to self, relative to base. 119 | 120 | Both self and base must be either relative or absolute; you cannot mix. 121 | 122 | Note: This method doesn't touch the filesystem. You must resolve symlinks, etc. your self first. 123 | """ 124 | "Algorithm taken from pathname.rb from Ruby 1.9.2" 125 | base = Pathname(base) 126 | 127 | if self.isabs() and base.isabs(): 128 | is_rel = False 129 | elif not self.isabs() and not base.isabs(): 130 | is_rel = True 131 | else: 132 | raise ValueError("self and base must both be relative or absolute!") 133 | 134 | a_prefix = self._path 135 | a_names = [] 136 | 137 | b_prefix = base._path 138 | b_names = [] 139 | 140 | a_prefix, basename = os.path.split(a_prefix) 141 | while a_prefix != '' and basename != '': 142 | if basename != os.path.curdir: 143 | a_names.insert(0, basename) 144 | a_prefix, basename = os.path.split(a_prefix) 145 | 146 | b_prefix, basename = os.path.split(b_prefix) 147 | while b_prefix != '' and basename != '': 148 | if basename != os.path.curdir: 149 | b_names.insert(0, basename) 150 | b_prefix, basename = os.path.split(b_prefix) 151 | 152 | if a_prefix != b_prefix: 153 | raise "different prefix: %r and %r" % (a_prefix, b_prefix) 154 | 155 | while a_names and b_names and a_names[0] == b_names[0]: 156 | a_names.pop(0) 157 | b_names.pop(0) 158 | 159 | if os.path.pardir in b_names: 160 | raise ValueError, "base includes .. in path: %r" % base 161 | 162 | b_names = [os.path.pardir for x in b_names] 163 | relpath = b_names + a_names 164 | 165 | if relpath: 166 | return Pathname(*relpath) 167 | else: 168 | return Pathname(os.path.curdir) 169 | 170 | def is_subdir_of(self, other): 171 | "Returns true if other is a subdirectory of self." 172 | other = Pathname(other) 173 | 174 | other_parts = [] 175 | other_prefix = other._path 176 | self_parts = [] 177 | self_prefix = self._path 178 | 179 | other_prefix, basename = os.path.split(other_prefix) 180 | while other_prefix != '' and basename != '': 181 | if basename != os.path.curdir: 182 | other_parts.insert(0, basename) 183 | other_prefix, basename = os.path.split(other_prefix) 184 | 185 | self_prefix, basename = os.path.split(self_prefix) 186 | while self_prefix != '' and basename != '': 187 | if basename != os.path.curdir: 188 | self_parts.insert(0, basename) 189 | self_prefix, basename = os.path.split(self_prefix) 190 | 191 | if other_prefix != self_prefix: 192 | raise "different prefix: %r and %r" % (self_prefix, other_prefix) 193 | 194 | other_path = os.path.sep.join(other_parts) + os.path.sep 195 | self_path = os.path.sep.join(self_parts) 196 | 197 | return self_path.startswith(other_path) 198 | 199 | if __name__ == "__main__": 200 | import unittest, re, tempfile 201 | 202 | class PathnameTestCase(unittest.TestCase): 203 | 204 | def testInit(self): 205 | p1 = Pathname('tmp', 'fish', 'blah') 206 | p2 = Pathname(p1) 207 | p3 = Pathname(*p2.split()) 208 | self.assertEqual(p1, p2) 209 | self.assertEqual(p1, p3) 210 | 211 | def testCompare(self): 212 | p1 = Pathname('tmp', 'fish', 'blah') 213 | p2 = Pathname(os.path.join('tmp', 'fish', 'blah')) 214 | self.assertEqual(p1, p2) 215 | 216 | p3 = Pathname('tmp', 'fish', 'blah', '') 217 | self.assertEqual(p1, p3) 218 | 219 | def testStr(self): 220 | expected = os.path.join('tmp','fish','blah') 221 | got = unicode(Pathname(expected)) 222 | self.assertEquals(got, expected) 223 | 224 | def testRepr(self): 225 | regex = re.compile('^<([A-Za-z]+)\s+id="([^"]+)"\s+path="([^"]+)">$') 226 | expected_path = os.path.join('tmp','fish','blah') 227 | p1 = Pathname(expected_path) 228 | self.assertTrue(p1 is not None) 229 | 230 | match = regex.search(repr(p1)) 231 | self.assertEqual(match.group(1), 'Pathname') 232 | p1_id = match.group(2) 233 | self.assertEqual(match.group(3), expected_path) 234 | 235 | p2 = Pathname(expected_path) 236 | match = regex.search(repr(p2)) 237 | self.assertEqual(match.group(1), 'Pathname') 238 | p2_id = match.group(2) 239 | self.assertEqual(match.group(3), expected_path) 240 | 241 | self.assertNotEqual(p1_id, p2_id) 242 | 243 | def testBasename(self): 244 | p = Pathname(os.path.join('tmp','fish','blah')) 245 | expected = 'blah' 246 | self.assertEqual(unicode(p.basename()), expected) 247 | 248 | def testDirname(self): 249 | p = Pathname(os.path.join('tmp','fish','blah')) 250 | expected = os.path.join('tmp','fish') 251 | self.assertEqual(unicode(p.dirname()), expected) 252 | 253 | def testAdd(self): 254 | p = Pathname(os.path.join('tmp','fish')) + 'blah' 255 | expected = os.path.join('tmp','fish','blah') 256 | self.assertEqual(unicode(p), expected) 257 | 258 | p = Pathname(os.path.join('tmp','fish')) + Pathname('blah') 259 | self.assertEqual(unicode(p), expected) 260 | 261 | p = Pathname('tmp') + 'fish' + 'blah' 262 | self.assertEqual(unicode(p), expected) 263 | 264 | def testIn(self): 265 | l = ('tmp', 'mouse', 'cat') 266 | for i in l: 267 | self.assertTrue(Pathname(i) in l) 268 | 269 | def testRelativePathFrom(self): 270 | s = os.path.sep 271 | pd = os.path.pardir 272 | cd = os.path.curdir 273 | p1a = Pathname(s, 'a','b','q',cd,'r') 274 | p2a = Pathname(s, 'a','b',cd,'c','d','e','f') 275 | p1r = Pathname('a','b','q','r',cd) 276 | p2r = Pathname('a',cd,'b','c','d','e','f') 277 | 278 | expected21 = Pathname(pd, pd, 'c','d','e','f') 279 | expected12 = Pathname(pd, pd, pd, pd, 'q', 'r') 280 | 281 | self.assertRaises(ValueError, p2a.relative_path_from, p1r) 282 | self.assertRaises(ValueError, p2r.relative_path_from, p1a) 283 | 284 | self.assertEqual(p2a.relative_path_from(p1a), 285 | expected21) 286 | self.assertEqual(p1a.relative_path_from(p2a), 287 | expected12) 288 | 289 | self.assertEqual(p2r.relative_path_from(p1r), 290 | expected21) 291 | self.assertEqual(p1r.relative_path_from(p2r), 292 | expected12) 293 | 294 | def testIsSubdirOf(self): 295 | s = os.path.sep 296 | cd = os.path.curdir 297 | p1a = Pathname(s,'a','b') 298 | p2a = Pathname(s,'a',cd,'b','c','d') 299 | 300 | p1r = Pathname('a',cd,'b') 301 | p2r = Pathname('a','b','c','d') 302 | 303 | self.assertFalse(p1a.is_subdir_of(p2a)) 304 | self.assertTrue (p2a.is_subdir_of(p1a)) 305 | 306 | self.assertFalse(p1r.is_subdir_of(p2r)) 307 | self.assertTrue (p2r.is_subdir_of(p1r)) 308 | 309 | def testIsSubdirOf2(self): 310 | top = Pathname(os.path.sep, 'Users','docwhat','.homedir','packages','homedir-examples','emacs-base') 311 | sub = Pathname(os.path.sep, 'Users','docwhat','.homedir','packages','homedir-examples','emacs-base','.emacs') 312 | 313 | self.assertTrue (sub.is_subdir_of(top)) 314 | self.assertFalse(top.is_subdir_of(sub)) 315 | 316 | def testIs(self): 317 | d = Pathname(tempfile.mkdtemp()) 318 | try: 319 | # Name them. 320 | dd = d + 'dir' 321 | ddl = d + 'dir_link' 322 | df = d + 'file' 323 | dfl = d + 'file_link' 324 | dbl = d + 'broken_link' 325 | 326 | # Make them. 327 | dd.mkdir() 328 | dd.symlink(ddl) 329 | 330 | f = df.open('w') 331 | f.write("text\n") 332 | f.close() 333 | df.symlink(dfl) 334 | 335 | (d + 'not-a-real-location').symlink(dbl) 336 | 337 | # Test them. 338 | self.assertTrue (dd.isdir()) 339 | self.assertFalse(dd.isfile()) 340 | self.assertFalse(dd.islink()) 341 | self.assertTrue (dd.exists()) 342 | 343 | self.assertTrue (ddl.isdir()) 344 | self.assertFalse(ddl.isfile()) 345 | self.assertTrue (ddl.islink()) 346 | self.assertTrue (dd.exists()) 347 | 348 | self.assertFalse(df.isdir()) 349 | self.assertTrue (df.isfile()) 350 | self.assertFalse(df.islink()) 351 | self.assertTrue (dd.exists()) 352 | 353 | self.assertFalse(dfl.isdir()) 354 | self.assertTrue (dfl.isfile()) 355 | self.assertTrue (dfl.islink()) 356 | self.assertTrue (dd.exists()) 357 | 358 | self.assertFalse(dbl.isdir()) 359 | self.assertFalse(dbl.isfile()) 360 | self.assertTrue (dbl.islink()) 361 | self.assertTrue (dd.exists()) 362 | 363 | finally: 364 | d.rm_rf() 365 | 366 | unittest.main() 367 | # === Core methods 368 | # 369 | # These methods are effectively manipulating a String, because that's 370 | # all a path is. Except for #mountpoint?, #children, #each_child, 371 | # #realdirpath and #realpath, they don't access the filesystem. 372 | # 373 | # - + 374 | # - #join 375 | # - #parent 376 | # - #root? 377 | # - #absolute? 378 | # - #relative? 379 | # - #relative_path_from 380 | # - #each_filename 381 | # - #cleanpath 382 | # - #realpath 383 | # - #realdirpath 384 | # - #children 385 | # - #each_child 386 | # - #mountpoint? 387 | # 388 | # === File status predicate methods 389 | # 390 | # These methods are a facade for FileTest: 391 | # - #blockdev? 392 | # - #chardev? 393 | # - #directory? 394 | # - #executable? 395 | # - #executable_real? 396 | # - #exist? 397 | # - #file? 398 | # - #grpowned? 399 | # - #owned? 400 | # - #pipe? 401 | # - #readable? 402 | # - #world_readable? 403 | # - #readable_real? 404 | # - #setgid? 405 | # - #setuid? 406 | # - #size 407 | # - #size? 408 | # - #socket? 409 | # - #sticky? 410 | # - #symlink? 411 | # - #writable? 412 | # - #world_writable? 413 | # - #writable_real? 414 | # - #zero? 415 | # 416 | # === File property and manipulation methods 417 | # 418 | # These methods are a facade for File: 419 | # - #atime 420 | # - #ctime 421 | # - #mtime 422 | # - #chmod(mode) 423 | # - #lchmod(mode) 424 | # - #chown(owner, group) 425 | # - #lchown(owner, group) 426 | # - #fnmatch(pattern, *args) 427 | # - #fnmatch?(pattern, *args) 428 | # - #ftype 429 | # - #make_link(old) 430 | # - #open(*args, &block) 431 | # - #readlink 432 | # - #rename(to) 433 | # - #stat 434 | # - #lstat 435 | # - #make_symlink(old) 436 | # - #truncate(length) 437 | # - #utime(atime, mtime) 438 | # - #basename(*args) 439 | # - #dirname 440 | # - #extname 441 | # - #expand_path(*args) 442 | # - #split 443 | 444 | 445 | 446 | -------------------------------------------------------------------------------- /lib/homedir/setup.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | """ 3 | This is a package management system designed to work around packages 4 | for the homedirectory. The code is based upon ideas from GNU Stow. 5 | 6 | HomeDir - manage the installation of packages for a user's homedir 7 | Copyright (C) 2004-2012 by Christian Höltje 8 | 9 | This program is free software; you can redistribute it and/or modify 10 | it under the terms of the GNU General Public License as published by 11 | the Free Software Foundation; either version 2 of the License, or 12 | (at your option) any later version. 13 | 14 | This program is distributed in the hope that it will be useful, but 15 | WITHOUT ANY WARRANTY; without even the implied warranty of 16 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 17 | General Public License for more details. 18 | 19 | You should have received a copy of the GNU General Public License 20 | along with this program; if not, write to the Free Software 21 | Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA. 22 | """ 23 | 24 | import os, sys 25 | import urllib2, httplib, tarfile, shutil, errno, subprocess 26 | try: 27 | from cStringIO import StringIO 28 | except ImportError: 29 | from StringIO import StringIO 30 | 31 | DIRVERSION=2 32 | class UnknownDirVersion(StandardError): 33 | pass 34 | 35 | def getVersion(): 36 | "Returns the latest version number from github" 37 | httplib.HTTPConnection.debuglevel = 1 38 | request = urllib2.Request("https://github.com/docwhat/homedir/raw/master/bin/homedir") 39 | opener = urllib2.build_opener() 40 | f = opener.open(request) 41 | version = None 42 | try: 43 | for line in f.readlines(): 44 | if line.startswith('VERSION="'): 45 | version = line.rstrip()[len('VERSION="'):-1] 46 | break 47 | finally: 48 | f.close() 49 | return version 50 | 51 | 52 | def getch(): 53 | "Returns a single character" 54 | if getch.platform is None: 55 | try: 56 | # Window's python? 57 | import msvcrt 58 | getch.platform = 'windows' 59 | except ImportError: 60 | # Fallback... 61 | try: 62 | import tty, termios 63 | fd = sys.stdin.fileno() 64 | old_settings = termios.tcgetattr(fd) 65 | getch.platform = 'unix' 66 | except termios.error: 67 | getch.platform = 'dumb' 68 | 69 | if getch.platform == 'windows': 70 | import msvcrt 71 | return msvcrt.getch() 72 | elif getch.platform == 'unix': 73 | import tty, termios 74 | fd = sys.stdin.fileno() 75 | old_settings = termios.tcgetattr(fd) 76 | try: 77 | tty.setraw(sys.stdin.fileno()) 78 | ch = sys.stdin.read(1) 79 | finally: 80 | termios.tcsetattr(fd, termios.TCSADRAIN, old_settings) 81 | return ch 82 | else: 83 | return sys.stdin.read(1).strip().lower() 84 | getch.platform = None 85 | 86 | # Monkey Patch os.makedirs 87 | def makedirs(name, mode=0777): 88 | """makedirs(path [, mode=0777]) 89 | 90 | Super-mkdir; create a leaf directory and all intermediate ones. 91 | Works like mkdir, except that any intermediate path segment (not 92 | just the rightmost) will be created if it does not exist. This is 93 | recursive. 94 | 95 | Monkey Patched. 96 | """ 97 | head, tail = os.path.split(name) 98 | if not tail: 99 | head, tail = os.path.split(head) 100 | if head and tail and not os.path.exists(head): 101 | try: 102 | makedirs(head, mode) 103 | except OSError, e: 104 | # be happy if someone already created the path 105 | if e.errno != errno.EEXIST: 106 | raise 107 | if tail == os.curdir: # xxx/newdir/. exists if xxx/newdir exists 108 | return 109 | try: 110 | os.mkdir(name, mode) 111 | except OSError, e: 112 | # be happy if someone already created the path 113 | if e.errno != errno.EEXIST: 114 | raise 115 | 116 | def copytree(src, dst): 117 | "A copy tree that doesn't copy .git, .svn, etc." 118 | names = os.listdir(src) 119 | makedirs(dst) 120 | errors = [] 121 | for name in names: 122 | if name in set(['.git', '.svn', 'CVS']) or name.endswith('.pyc'): 123 | continue 124 | srcname = os.path.join(src, name) 125 | dstname = os.path.join(dst, name) 126 | try: 127 | if os.path.islink(srcname): 128 | linkto = os.readlink(srcname) 129 | os.symlink(linkto, dstname) 130 | elif os.path.isdir(srcname): 131 | copytree(srcname, dstname) 132 | else: 133 | shutil.copy2(srcname, dstname) 134 | except (IOError, os.error), why: 135 | errors.append((srcname, dstname, str(why))) 136 | except StandardError, err: 137 | errors.extend(err.args[0]) 138 | try: 139 | shutil.copystat(src, dst) 140 | except OSError, why: 141 | if WindowsError is not None and isinstance(why, WindowsError): 142 | # Copying file access times may fail on Windows 143 | pass 144 | else: 145 | errors.extend((src, dst, str(why))) 146 | if errors: 147 | raise Exception, errors 148 | 149 | def msg(s): 150 | print " * %s" % s 151 | 152 | class MyTarFile(tarfile.TarFile): 153 | 154 | def extract(self, member, path=""): 155 | "Modified version of extract that strips out the leading path part." 156 | self._check('r') 157 | if not isinstance(member, tarfile.TarInfo): 158 | member = self.getmember(member) 159 | 160 | if member.islnk(): 161 | fname = member.linkname 162 | else: 163 | fname = member.name 164 | if not fname == 'pax_global_header': 165 | for i in member.name: 166 | if fname[0] in (os.path.sep, os.path.altsep): 167 | fname = fname[1:] 168 | break 169 | fname = fname[1:] 170 | 171 | if member.islnk(): 172 | member.linkname = fname 173 | else: 174 | member.name = fname 175 | 176 | return tarfile.TarFile.extract(self, member, path) 177 | return True 178 | 179 | class Setup: 180 | "Setup/Install/Configure Homedir." 181 | 182 | def __init__(self, via_web, directory=None): 183 | if directory is None: 184 | directory = os.path.expanduser("~/.homedir") 185 | self.dir = directory 186 | 187 | self.via_web = via_web 188 | 189 | print "Setting up HomeDir!" 190 | print 191 | 192 | # Here's the script... 193 | self.createDir() 194 | self.getFiles() 195 | self.installFiles() 196 | self.fixHashBang() 197 | self.cleanup() 198 | msg("Done!") 199 | 200 | print 201 | bindir = os.path.expanduser("~/bin") 202 | if bindir in os.getenv('PATH', '').split(os.pathsep): 203 | print "You're all setup and can now run the command 'homedir'." 204 | else: 205 | print "HomeDir is installed, however $HOME/bin is not in your PATH." 206 | print "You can either manually run HomeDir from your directory or" 207 | print "you can modify your shell startup scripts to include ~/bin" 208 | 209 | def detectDirVersion(self): 210 | "Detects the version of the .homedir directory" 211 | """ 212 | Versions: 213 | 0 - No version. 214 | 1 - Original Homedir as hosted on http://trac.gerf.org/homedir 215 | 2 - New Homedir. 216 | """ 217 | pj = os.path.join 218 | if not os.path.isdir(self.dir): 219 | return 0 220 | if os.path.isdir(pj(self.dir, 'files')) and \ 221 | os.path.isdir(pj(self.dir, 'cache')): 222 | return 1 223 | if os.path.isfile(pj(self.dir, '.version')): 224 | f = file(pj(self.dir, '.version')) 225 | try: 226 | version = f.readline().strip() 227 | finally: 228 | f.close() 229 | return int(version) 230 | 231 | print UnknownDirVersion("I got no clue what your .homedir is.") 232 | raise UnknownDirVersion("I got no clue what your .homedir is.") 233 | 234 | def createDir(self): 235 | "Create the directory if it doesn't exist. Offer to clean or purge it if it does." 236 | try: 237 | version = self.detectDirVersion() 238 | except UnknownDirVersion: 239 | print "You already have a directory called %s" % self.dir 240 | print "but I don't know what it is." 241 | print 242 | print "I can..." 243 | print " ...purge the directory, removing the current contents." 244 | print " or" 245 | print " ...quit and let you fix things the way you want." 246 | print 247 | 248 | answer = None 249 | while answer not in ['p', 'q']: 250 | if answer is not None: 251 | print 252 | print 253 | print "Please press one of the following letters: p q" 254 | print 255 | sys.stdout.write('Should I [p]urge it, or just [q]uit? ') 256 | answer = getch().strip().lower() 257 | 258 | print 259 | if answer == 'p': 260 | self.purgeDir(self) 261 | version = 0 262 | else: 263 | print "Goodbye!" 264 | sys.exit(0) 265 | 266 | msg("Updating directory...") 267 | while version < DIRVERSION: 268 | getattr(self, "updateVersion%d" % version)() 269 | version += 1 270 | f = file(os.path.join(self.dir, '.version'), 'w') 271 | try: 272 | f.write("%d%s" % (DIRVERSION, os.linesep)) 273 | finally: 274 | f.close() 275 | 276 | def purgeDir(self): 277 | "Deletes the current ~/.homedir directory." 278 | shutil.rmtree(os.path.join(self.dir), ignore_errors=True) 279 | 280 | def getFiles(self): 281 | "Copy files instead of getting them from the web." 282 | shutil.rmtree(os.path.join(self.dir, 'tmp'), ignore_errors=True) 283 | dst=os.path.join(self.dir, 'tmp') 284 | 285 | if self.via_web: 286 | msg("Fetching latest version of homedir...") 287 | httplib.HTTPConnection.debuglevel = 1 288 | request = urllib2.Request("https://github.com/docwhat/homedir/tarball/master") 289 | opener = urllib2.build_opener() 290 | f = opener.open(request) 291 | try: 292 | z = MyTarFile.open(fileobj=f, mode='r|*') 293 | z.extractall(dst) 294 | finally: 295 | f.close() 296 | else: 297 | msg("Copying homedir from files...") 298 | src = os.path.abspath(__file__) 299 | for i in range(3): 300 | src = os.path.dirname(src) 301 | copytree(src, dst) 302 | 303 | # Patch the version in homedir... 304 | msg("Patching version...") 305 | data = [] 306 | f = file(os.path.join(dst, 'bin', 'homedir'), 'r') 307 | try: 308 | for line in f.readlines(): 309 | line = line.rstrip() 310 | if line.startswith('VERSION="'): 311 | ver = line[len('VERSION="'):-1] 312 | line = 'VERSION="%s-unofficial"' % ver 313 | data.append(line) 314 | finally: 315 | f.close() 316 | 317 | f = file(os.path.join(dst, 'bin', 'homedir'), 'w') 318 | try: 319 | for line in data: 320 | f.write(line) 321 | f.write("\n") 322 | finally: 323 | f.close() 324 | 325 | def installFiles(self): 326 | "This copies the files into their proper location in .homedir and symlinks them into $HOME" 327 | msg("Installing files...") 328 | pj = os.path.join 329 | for e in ('bin', 'lib', 'cache'): 330 | src = pj(self.dir, 'tmp', e) 331 | dst = pj(self.dir, e) 332 | shutil.rmtree(dst, ignore_errors=True) 333 | copytree(src, dst) 334 | 335 | bindir = os.path.expanduser("~/bin") 336 | makedirs(bindir) 337 | for e in os.listdir(pj(self.dir, 'bin')): 338 | src = pj(os.path.pardir, '.homedir', 'bin', e) 339 | dst = pj(bindir, e) 340 | if os.path.islink(dst): 341 | os.unlink(dst) 342 | os.symlink(src, dst) 343 | 344 | def fixHashBang(self): 345 | "Go through all the python scripts and fix the hash-bangs." 346 | msg("Fixing hashbangs...") 347 | bindir = os.path.join(self.dir, 'bin') 348 | for entry in os.listdir(bindir): 349 | p = os.path.join(bindir, entry) 350 | if os.path.isfile(p): 351 | fd = file(p, 'r') 352 | hashbang = fd.readline() 353 | if hashbang.startswith('#!') and 'python' in hashbang: 354 | data = "#!%s -utWall\n" % (sys.executable) + fd.read() 355 | fd.close() 356 | fd = file(p, 'w') 357 | fd.write(data) 358 | fd.close() 359 | 360 | def cleanup(self): 361 | pj = os.path.join 362 | shutil.rmtree(pj(self.dir, 'tmp'), ignore_errors=True) 363 | 364 | def updateVersion0(self): 365 | "Creates a base .homedir" 366 | makedirs(self.dir) 367 | 368 | def updateVersion1(self): 369 | "Updates from old style homedir to the new layout." 370 | pj = os.path.join 371 | makedirs(pj(self.dir, 'bin')) 372 | makedirs(pj(self.dir, 'lib')) 373 | makedirs(pj(self.dir, 'packages')) 374 | if os.path.isfile(pj(self.dir, 'config')): 375 | os.rename(pj(self.dir, 'config'), pj(self.dir, 'config-is-nolonger-used')) 376 | 377 | # We don't need these. 378 | shutil.rmtree(pj(self.dir, 'files', 'unittest'), ignore_errors=True) 379 | if os.path.isfile(pj(self.dir, 'files', 'setup')): 380 | os.unlink(pj(self.dir, 'files', 'setup')) 381 | 382 | if os.path.isdir(pj(self.dir, 'files')): 383 | os.rename(pj(self.dir, 'files'), pj(self.dir, 'packages')) 384 | 385 | old_homedir = pj(self.dir, 'packages', 'packages', '00homedir') 386 | if os.path.isdir(old_homedir): 387 | shutil.rmtree(old_homedir, ignore_errors=True) 388 | 389 | #shutil.rmtree(pj(self.dir, 'cache'), ignore_errors=True) 390 | 391 | 392 | if __name__ == "__main__": 393 | if sys.version_info < (2,5): 394 | print >> sys.stderr, "This program requires python 2.5.\nYou're running python %s" % sys.version 395 | sys.exit(1) 396 | Setup(via_web=("" == __file__)) 397 | 398 | # vim: set sw=4 ts=4 expandtab 399 | --------------------------------------------------------------------------------