From b07d5c977613ac7a20ccfb7cd5bff16845279151 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Eray=20=C3=96zkural?= Date: Sat, 18 Jun 2005 21:15:35 +0000 Subject: [PATCH] * dir'leri yeniden organize ediyoruz, bir sonraki adim python package'i yapmak tam olarak --- lib/pisi/__init__.py | 4 + lib/pisi/actionsapi/__init__.py | 3 + lib/pisi/actionsapi/autotools.py | 45 ++++++ lib/pisi/actionsapi/gnuconfig.py | 48 ++++++ lib/pisi/actionsapi/libtoolize.py | 14 ++ lib/pisi/actionsapi/shell.py | 63 ++++++++ lib/pisi/actionsapi/utils.py | 17 +++ .../var_lib_pisi/portage-1.4.1.patch | 74 +++++++++ .../actionsapi/var_lib_pisi/sed-1.4.0.patch | 14 ++ lib/pisi/archive.py | 103 +++++++++++++ lib/pisi/build.py | 132 ++++++++++++++++ lib/pisi/colors.py | 48 ++++++ lib/pisi/context.py | 104 +++++++++++++ lib/pisi/dependency.py | 3 + lib/pisi/fetcher.py | 139 +++++++++++++++++ lib/pisi/install.py | 46 ++++++ lib/pisi/installdb.py | 52 +++++++ lib/pisi/oo.py | 20 +++ lib/pisi/package.py | 30 ++++ lib/pisi/packagedb.py | 28 ++++ lib/pisi/sourcedb.py | 18 +++ lib/pisi/specfile.py | 105 +++++++++++++ lib/pisi/ui.py | 30 ++++ lib/pisi/util.py | 69 +++++++++ lib/pisi/xmlfile.py | 141 ++++++++++++++++++ 25 files changed, 1350 insertions(+) create mode 100644 lib/pisi/__init__.py create mode 100644 lib/pisi/actionsapi/__init__.py create mode 100644 lib/pisi/actionsapi/autotools.py create mode 100644 lib/pisi/actionsapi/gnuconfig.py create mode 100644 lib/pisi/actionsapi/libtoolize.py create mode 100644 lib/pisi/actionsapi/shell.py create mode 100644 lib/pisi/actionsapi/utils.py create mode 100644 lib/pisi/actionsapi/var_lib_pisi/portage-1.4.1.patch create mode 100644 lib/pisi/actionsapi/var_lib_pisi/sed-1.4.0.patch create mode 100644 lib/pisi/archive.py create mode 100644 lib/pisi/build.py create mode 100644 lib/pisi/colors.py create mode 100644 lib/pisi/context.py create mode 100644 lib/pisi/dependency.py create mode 100644 lib/pisi/fetcher.py create mode 100644 lib/pisi/install.py create mode 100644 lib/pisi/installdb.py create mode 100644 lib/pisi/oo.py create mode 100644 lib/pisi/package.py create mode 100644 lib/pisi/packagedb.py create mode 100644 lib/pisi/sourcedb.py create mode 100644 lib/pisi/specfile.py create mode 100644 lib/pisi/ui.py create mode 100644 lib/pisi/util.py create mode 100644 lib/pisi/xmlfile.py diff --git a/lib/pisi/__init__.py b/lib/pisi/__init__.py new file mode 100644 index 00000000..2816f232 --- /dev/null +++ b/lib/pisi/__init__.py @@ -0,0 +1,4 @@ + +# PISI package version +__version__ = "0.1" + diff --git a/lib/pisi/actionsapi/__init__.py b/lib/pisi/actionsapi/__init__.py new file mode 100644 index 00000000..5fee121d --- /dev/null +++ b/lib/pisi/actionsapi/__init__.py @@ -0,0 +1,3 @@ + +# pisi.actionsapi version +__version__ = "0.1" diff --git a/lib/pisi/actionsapi/autotools.py b/lib/pisi/actionsapi/autotools.py new file mode 100644 index 00000000..89136558 --- /dev/null +++ b/lib/pisi/actionsapi/autotools.py @@ -0,0 +1,45 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- + +import os +from pisi.context import Constants +const = Constants.c + +def configure(parameters = None): + ''' FIXME: Düzgün hale getirilecek ''' + ''' {EXTRA} = '--with-nls --with-libusb --with-something-usefull ''' + + # FIXME: I don't think its feasible to write all these parameters + # here. There should be a way to get all these... pisi.context.Constants? + configure_string = './configure --prefix=/usr \ + --host=i686-pc-linux-gnu \ + --mandir=/usr/share/man \ + --infodir=/usr/share/info \ + --datadir=/usr/share \ + --sysconfdir=/etc \ + --localstatedir=/var/lib {EXTRA}' + + cmd = configure_string.replace('{EXTRA}', parameters) + os.system(cmd) + +def make(): + ''' FIXME: Düzgün hale getirilecek ''' + os.system('make') + +def install(): + ''' FIXME: Düzgün hale getirilecek ''' + ''' {D} = /var/tmp/pisi/ _paket_adı_ /image/ ''' + global const + + install_string = 'make prefix={D}/usr \ + datadir={D}/usr/share \ + infodir={D}/usr/share/info \ + localstatedir={D}/var/lib \ + mandir={D}/usr/share/man \ + sysconfdir={D}/etc \ + install' + + cmd = os.path.dirname(os.path.dirname(os.getcwd())) + \ + const.install_dir_suffix + cmd = install_string.replace('{D}', cmd) + os.system(cmd) diff --git a/lib/pisi/actionsapi/gnuconfig.py b/lib/pisi/actionsapi/gnuconfig.py new file mode 100644 index 00000000..36722c78 --- /dev/null +++ b/lib/pisi/actionsapi/gnuconfig.py @@ -0,0 +1,48 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- + +import os, string, re, shutil +from shell import * + +def gnuconfig_findnewest(): + ''' find the newest config.* file according to + timestamp and return it''' + + locations = ['/usr/share/gnuconfig/config.sub', + '/usr/share/automake-1.8/config.sub', + '/usr/share/automake-1.7/config.sub', + '/usr/share/automake-1.6/config.sub', + '/usr/share/automake-1.5/config.sub', + '/usr/share/automake-1.4/config.sub'] + + newer_location = {} + + for i in locations: + newer_location[i] = re.sub('\'', + '', + string.split((cat(i) | + tr(str.rstrip) | + grep ('^timestamp') | + join), '=')[1]) + + keys = newer_location.keys() + keys.sort() + map(newer_location.get, keys) + + return os.path.dirname(newer_location.popitem()[0]) + +def gnuconfig_update(): + ''' copy newest config.* onto source's ''' + + newer_location = gnuconfig_findnewest() + + try: + shutil.copyfile(newer_location + '/config.sub', + os.getcwd() + '/config.sub') + shutil.copyfile(newer_location + '/config.guess', + os.getcwd() + '/config.guess') + except IOError: + print 'Hata mata...' + sys.exit() + + print 'GNU Config Update Finished...' diff --git a/lib/pisi/actionsapi/libtoolize.py b/lib/pisi/actionsapi/libtoolize.py new file mode 100644 index 00000000..47ef40fd --- /dev/null +++ b/lib/pisi/actionsapi/libtoolize.py @@ -0,0 +1,14 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- + +import os + +def libtoolize(): + ''' FIXME: Düzgün hale getirilecek ''' + ''' patch source with ltmain patches ''' + + # This is wrong! Action files should only operate on the build + # directory. And shouldn't depend on external files. If a patch is + # need to be applied it is PisiBuild's job to do it! + os.system('patch -sN < ' + Context.lib_dir() + '/portage-1.4.1.patch') + os.system('patch -sN < ' + Context.lib_dir() + '/sed-1.4.0.patch') diff --git a/lib/pisi/actionsapi/shell.py b/lib/pisi/actionsapi/shell.py new file mode 100644 index 00000000..e82d7557 --- /dev/null +++ b/lib/pisi/actionsapi/shell.py @@ -0,0 +1,63 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- + +import sys, re +from itertools import izip, imap, count, ifilter, ifilterfalse + +def cat(filename): + return file(filename).xreadlines() + +class grep: + """keep only lines that match the regexp""" + def __init__(self,pat, flags = 0): + self.fun = re.compile(pat, flags).match + def __ror__(self, input): + return ifilter(self.fun, input) + +class tr: + """apply arbitrary transform to each sequence element""" + def __init__(self, transform): + self.tr = transform + def __ror__(self, input): + return imap(self.tr, input) + +class printto: + """print sequence elements one per line""" + def __init__(self, out = sys.stdout): + self.out = out + def __ror__(self,input): + for l in input: + print >> self.out, l + +printlines = printto(sys.stdout) + +class terminator: + def __init__(self,method): + self.process = method + def __ror__(self,input): + return self.process(input) + +aslist = terminator(list) +asdict = terminator(dict) +astuple = terminator(tuple) +join = terminator("".join) +enum = terminator(enumerate) + +class sort: + def __ror__(self,input): + ll = list(input) + ll.sort() + return ll +sort = sort() + +class uniq: + def __ror__(self,input): + for i in input: + try: + if i == prev: + continue + except NameError: + pass + prev = i + yield i +uniq = uniq() diff --git a/lib/pisi/actionsapi/utils.py b/lib/pisi/actionsapi/utils.py new file mode 100644 index 00000000..e5022884 --- /dev/null +++ b/lib/pisi/actionsapi/utils.py @@ -0,0 +1,17 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- + +import time +from tempfile import mkstemp, mkdtemp + +def sleep(sleep_time = 5): + time.sleep(sleep_time) + +def createTmpFile(): + handle, path = mkstemp() + return path + +def createTmpDir(): + path = mkdtemp() + return path + diff --git a/lib/pisi/actionsapi/var_lib_pisi/portage-1.4.1.patch b/lib/pisi/actionsapi/var_lib_pisi/portage-1.4.1.patch new file mode 100644 index 00000000..a49f3332 --- /dev/null +++ b/lib/pisi/actionsapi/var_lib_pisi/portage-1.4.1.patch @@ -0,0 +1,74 @@ +Note that if you update this patch, please update this one as well: + + eclass/ELT-patches/portage/1.4.1 + +The file name can stay 1.4.1, as it will still apply to all versions. Only +when a new version of libtool comes out that it do not apply to, then the +name should be bumped, but the patch content should stay fairly the same. + +--- ltmain.sh Wed Apr 3 01:19:37 2002 ++++ ltmain.sh Sun May 26 19:50:52 2002 +@@ -3940,9 +3940,50 @@ + $echo "$modename: \`$deplib' is not a valid libtool archive" 1>&2 + exit 1 + fi +- newdependency_libs="$newdependency_libs $libdir/$name" ++ # We do not want portage's install root ($D) present. Check only for ++ # this if the .la is being installed. ++ if test "$installed" = yes && test "$D"; then ++ eval mynewdependency_lib=`echo "$libdir/$name" |sed -e "s:$D:/:g" -e 's:/\+:/:g'` ++ else ++ mynewdependency_lib="$libdir/$name" ++ fi ++ # Do not add duplicates ++ if test "$mynewdependency_lib"; then ++ my_little_ninja_foo_1=`echo $newdependency_libs |$EGREP -e "$mynewdependency_lib"` ++ if test -z "$my_little_ninja_foo_1"; then ++ newdependency_libs="$newdependency_libs $mynewdependency_lib" ++ fi ++ fi ++ ;; ++ *) ++ if test "$installed" = yes; then ++ # Rather use S=WORKDIR if our version of portage supports it. ++ # This is because some ebuild (gcc) do not use $S as buildroot. ++ if test "$PWORKDIR"; then ++ S="$PWORKDIR" ++ fi ++ # We do not want portage's build root ($S) present. ++ my_little_ninja_foo_2=`echo $deplib |$EGREP -e "$S"` ++ if test -n "$my_little_ninja_foo_2" && test "$S"; then ++ mynewdependency_lib="" ++ # We do not want portage's install root ($D) present. ++ my_little_ninja_foo_3=`echo $deplib |$EGREP -e "$D"` ++ elif test -n "$my_little_ninja_foo_3" && test "$D"; then ++ eval mynewdependency_lib=`echo "$deplib" |sed -e "s:$D:/:g" -e 's:/\+:/:g'` ++ else ++ mynewdependency_lib="$deplib" ++ fi ++ else ++ mynewdependency_lib="$deplib" ++ fi ++ # Do not add duplicates ++ if test "$mynewdependency_lib"; then ++ my_little_ninja_foo_4=`echo $newdependency_libs |$EGREP -e "$mynewdependency_lib"` ++ if test -z "$my_little_ninja_foo_4"; then ++ newdependency_libs="$newdependency_libs $mynewdependency_lib" ++ fi ++ fi + ;; +- *) newdependency_libs="$newdependency_libs $deplib" ;; + esac + done + dependency_libs="$newdependency_libs" +@@ -3975,6 +4005,10 @@ + case $host,$output,$installed,$module,$dlname in + *cygwin*,*lai,yes,no,*.dll) tdlname=../bin/$dlname ;; + esac ++ # Do not add duplicates ++ if test "$installed" = yes && test "$D"; then ++ install_libdir=`echo "$install_libdir" |sed -e "s:$D:/:g" -e 's:/\+:/:g'` ++ fi + $echo > $output "\ + # $outputname - a libtool library file + # Generated by $PROGRAM - GNU $PACKAGE $VERSION$TIMESTAMP diff --git a/lib/pisi/actionsapi/var_lib_pisi/sed-1.4.0.patch b/lib/pisi/actionsapi/var_lib_pisi/sed-1.4.0.patch new file mode 100644 index 00000000..9c51ac2d --- /dev/null +++ b/lib/pisi/actionsapi/var_lib_pisi/sed-1.4.0.patch @@ -0,0 +1,14 @@ +--- ltmain.sh 2003-02-13 14:54:24.000000000 +0100 ++++ ltmain.sh 2003-02-13 15:24:49.000000000 +0100 +@@ -48,6 +48,11 @@ EOF + exit 0 + fi + ++# define variables for historic ltconfig's generated by Libtool 1.3 ++test -z "$SED" && SED=sed ++test -z "$EGREP" && EGREP=egrep ++test -z "$LTCC" && LTCC=${CC-gcc} ++ + # The name of this program. + progname=`$echo "$0" | ${SED} 's%^.*/%%'` + modename="$progname" diff --git a/lib/pisi/archive.py b/lib/pisi/archive.py new file mode 100644 index 00000000..da01c1c1 --- /dev/null +++ b/lib/pisi/archive.py @@ -0,0 +1,103 @@ +# -*- coding: utf-8 -*- +# unpack magic +# maintainer baris and meren + +#standart lisbrary modules +import os +import sys +import tarfile +import zipfile + +#pisi modules +import util + +class ArchiveBase(object): + def __init__(self, ctx): + self.type = ctx.spec.source.archiveType + self.fileName = os.path.basename(ctx.spec.source.archiveUri) + self.filePath = ctx.archives_dir() + '/' + self.fileName + + def unpack(self, targetDir): + self.targetDir = targetDir + # first we check if we need to clean-up our working env. + if os.path.exists(self.targetDir): + util.clean_dir(self.targetDir) + else: + os.makedirs(self.targetDir) + +class ArchiveTarFile(ArchiveBase): + def __init__(self, ctx): + super(ArchiveTarFile, self).__init__(ctx) + + def unpack(self, targetDir): + super(ArchiveTarFile, self).unpack(targetDir) + + rmode = "" + if self.type == 'tar': + rmode = 'r:' + elif self.type == 'targz': + rmode = 'r:gz' + elif self.type == 'tarbz2': + rmode = 'r:bz2' + tar = tarfile.open(self.filePath, rmode) + oldwd = os.getcwd() + os.chdir(self.targetDir) + for tarinfo in tar: + tar.extract(tarinfo) + os.chdir(oldwd) + tar.close() + +class ArchiveZip(ArchiveBase): + def __init__(self, ctx): + super(ArchiveZip, self).__init__(ctx) + + def unpack(self, targetDir): + super(ArchiveZip, self).unpack(targetDir) + + zip = zipfile.ZipFile(self.filePath, 'r') + for file in zip.namelist(): + ofile = self.targetDir + '/' + file + + # a directory is present. lets continue + if os.path.isdir(ofile): + continue + # do we need to create parent directory for our file? + if not os.path.exists(os.path.dirname(ofile)): + os.mkdir(ofile) + continue + info = zip.getinfo(file) + # O.K. we know following line is dull. What we wanted to + # do was to compare the equality to 0xa0000000. But there + # is a known problem in Python regarding the hex/oct + # constants. Please see Guido's explanation at + # http://mail.python.org/pipermail/python-dev/2003-February/033029.html + if hex(info.external_attr)[2] == 'A': + target = zip.read(file) + os.symlink(target, ofile) + else: + buff = open (ofile, 'wb') + fileContent = zip.read(file) + buff.write(fileContent) + buff.close() + + zip.close() + +class Archive: + """Unpack magic for Archive files...""" + + def __init__(self, ctx): + """accepted archive types: + targz, tarbz2, zip, tar""" + + handlers = { + 'targz': ArchiveTarFile, + 'tarbz2': ArchiveTarFile, + 'tar': ArchiveTarFile, + 'zip': ArchiveZip + } + + type = ctx.spec.source.archiveType + self.archive = handlers.get(type)(ctx) + + def unpack(self, targetDir): + self.archive.unpack(targetDir) diff --git a/lib/pisi/build.py b/lib/pisi/build.py new file mode 100644 index 00000000..db3a150e --- /dev/null +++ b/lib/pisi/build.py @@ -0,0 +1,132 @@ +# -*- coding: utf-8 -*- +# package bulding stuff +# maintainer: baris and meren + +# python standard library +import os + +from fetcher import Fetcher +from archive import Archive + +# import pisipackage +import util +from ui import ui + +class PisiBuildError(Exception): + pass + +# FIXME: this eventually has to go to ui module +# Infact all ui calls has nothing to do with this build process. +# There more of them in PisiBuild... +# And maybe we should consider moving PisiBuild.build() back to pisi-build +# CLI too. +# exa: This, like all others, will have a GUI or CLI, interchangeably +def displayProgress(pd): + out = '\r%-30.30s %3d%% %12.2f %s' % \ + (pd['filename'], pd['percent'], pd['rate'], pd['symbol']) + ui.info(out) + +class PisiBuild: + """PisiBuild class, provides the package build and creation routines""" + def __init__(self, context): + self.ctx = context + self.work_dir = self.ctx.pkg_work_dir() + + self.spec = self.ctx.spec + + def build(self): + ui.info("Building PISI source package: %s\n" % self.spec.source.name) + + ui.info("Fetching source from: %s\n" % self.spec.source.archiveUri) + self.fetchArchive(displayProgress) + ui.info("Source archive is stored: %s/%s\n" + %(self.ctx.archives_dir(), self.spec.source.archiveName)) + + self.solveBuildDependencies() + + ui.info("Unpacking archive...") + self.unpackArchive() + ui.info(" unpacked (%s)\n" % self.ctx.pkg_work_dir()) + + self.applyPatches() + + try: + specdir = os.path.dirname(self.ctx.pspecfile) + self.actionScript = open("/".join([specdir,self.ctx.const.actions_file])).read() + except IOError, e: + ui.error ("Action Script: %s\n" % e) + return + + # FIXME: It's wrong to assume that unpacked archive + # will create a name-version top-level directory. + # Archive module should give the exact location. + # (from the assumption is evil dept.) + os.chdir(self.ctx.pkg_work_dir() + "/" + self.spec.source.name + "-" + self.spec.source.version) + locals = globals = {} + + try: + exec compile(self.actionScript , "error", "exec") in locals,globals + except SyntaxError, e: + ui.error ("Error : %s\n" % e) + return + + self.configureSource(locals) + self.buildSource(locals) + self.installSource(locals) + + # after all, we are ready to build/prepare the packages + self.buildPackages() + + def fetchArchive(self, percentHook=None): + """fetch an archive and store to ctx.archives_dir() + using fether.Fetcher""" + fetch = Fetcher(self.ctx) + + # check if source already cached + destpath = fetch.filedest + "/" + fetch.filename + if os.access(destpath, os.R_OK): + if util.md5_file(destpath) == self.spec.source.archiveMD5: + ui.info('%s [cached]\n' % self.spec.source.archiveName) + return + + if percentHook: + fetch.percentHook = percentHook + + fetch.fetch() + + # FIXME: What a ugly hack! We should really find a cleaner way for output. + if percentHook: + ui.info('\n') + + def solveBuildDependencies(self): + pass + + def unpackArchive(self): + archive = Archive(self.ctx) + archive.unpack(self.work_dir) + + def applyPatches(self): + pass + + def configureSource(self, locals): + func = self.ctx.const.setup_func + if func in locals: + ui.info("Configuring %s...\n" % self.spec.source.name) + locals[func]() + + def buildSource(self, locals): + func = self.ctx.const.build_func + if func in locals: + ui.info("Building %s...\n" % self.spec.source.name) + locals[func]() + + def installSource(self, locals): + func = self.ctx.const.install_func + if func in locals: + ui.info("Installing %s...\n" % self.spec.source.name) + locals[func]() + + def buildPackages(self): + for package in self.spec.packages: + ui.info("** Building package %s\n" % package.name); + diff --git a/lib/pisi/colors.py b/lib/pisi/colors.py new file mode 100644 index 00000000..835ddced --- /dev/null +++ b/lib/pisi/colors.py @@ -0,0 +1,48 @@ + +colors = {'black' : "\033[30m", + 'red' : "\033[31m", + 'green' : "\033[32m", + 'yellow' : "\033[33m", + 'blue' : "\033[34m", + 'purple' : "\033[35m", + 'cyan' : "\033[36m", + 'white' : "\033[37m", + 'brightblack' : "\033[01;30m", + 'brightred' : "\033[01;31m", + 'brightgreen' : "\033[01;32m", + 'brightyellow' : "\033[01;33m", + 'brightblue' : "\033[01;34m", + 'brightmagenta' : "\033[01;35m", + 'brightcyan' : "\033[01;36m", + 'brightwhite' : "\033[01;37m", + 'underlineblack' : "\033[04;30m", + 'underlinered' : "\033[04;31m", + 'underlinegreen' : "\033[04;32m", + 'underlineyellow' : "\033[04;33m", + 'underlineblue' : "\033[04;34m", + 'underlinemagenta' : "\033[04;35m", + 'underlinecyan' : "\033[04;36m", + 'underlinewhite' : "\033[04;37m", + 'blinkingblack' : "\033[05;30m", + 'blinkingred' : "\033[05;31m", + 'blinkinggreen' : "\033[05;32m", + 'blinkingyellow' : "\033[05;33m", + 'blinkingblue' : "\033[05;34m", + 'blinkingmagenta' : "\033[05;35m", + 'blinkingcyan' : "\033[05;36m", + 'blinkingwhite' : "\033[05;37m", + 'backgroundblack' : "\033[07;30m", + 'backgroundred' : "\033[07;31m", + 'backgroundgreen' : "\033[07;32m", + 'backgroundyellow' : "\033[07;33m", + 'backgroundblue' : "\033[07;34m", + 'backgroundmagenta' : "\033[07;35m", + 'backgroundcyan' : "\033[07;36m", + 'backgroundwhite' : "\033[07;37m", + 'default' : "\033[0m" } + +def colorize(msg, color): + if colors.has_key(color): + return colors[color] + msg + colors['default'] + else: + return msg diff --git a/lib/pisi/context.py b/lib/pisi/context.py new file mode 100644 index 00000000..f95c4e92 --- /dev/null +++ b/lib/pisi/context.py @@ -0,0 +1,104 @@ +# -*- coding: utf-8 -*- +# PISI configuration (static and dynamic) + +from specfile import SpecFile +import oo + +class Constants: + "Pisi constants" + + c = oo.const() + + def __init__(self): + self.c.lib_dir_suffix = "/var/lib/pisi" + self.c.db_dir_suffix = "/var/db/pisi" + self.c.archives_dir_suffix = "/var/cache/pisi/archives" + self.c.tmp_dir_suffix = "/var/tmp/pisi" + + # directory suffixes for build + self.c.work_dir_suffix = "/work" + self.c.install_dir_suffix = "/install" + + # file/directory names + self.c.actions_file = "actions.py" + self.c.files_dir = "files" + + # functions in actions_file + self.c.setup_func = "setup" + self.c.build_func = "build" + self.c.install_func = "install" + + def __getattr__(self, attr): + return getattr(self.c, attr) + + def __setattr__(self, attr, value): + return setattr(self.c, attr, value) + + def __delattr__(self, attr): + return delattr(self.c, attr) + +class Context(object): + """Config/Context Singleton""" + class __impl: + def __init__(self): + self.const = Constants() + # self.c.destdir = '' # install default to root by default + self.destdir = './tmp' # only for ALPHA + # the idea is that destdir can be set with --destdir=... + + def setSpecFile(self, pspecfile): + self.pspecfile = pspecfile + spec = SpecFile() + spec.read(pspecfile) + spec.verify() # check pspec integrity + self.spec = spec + + # directory accessor functions + # here is how it goes + # x_dir: system wide directory for storing info type x + # pkg_x_dir: per package directory for storing info type x + + def lib_dir(self): + return self.destdir + self.const.lib_dir_suffix + + def db_dir(self): + return self.destdir + self.const.db_dir_suffix + + def archives_dir(self): + return self.destdir + self.const.archives_dir_suffix + + def tmp_dir(self): + return self.destdir + self.const.tmp_dir_suffix + + def pkg_work_dir(self): + packageDir = self.spec.source.name + '-' \ + + self.spec.source.version + '-' + self.spec.source.release + + return self.destdir + self.const.tmp_dir_suffix \ + + '/' + packageDir + self.const.work_dir_suffix + + def install_dir(self): + return self.tmp_dir() + self.const.install_dir_suffix + + def pkg_install_dir(self): + packageDir = self.spec.source.name + '-' \ + + self.spec.source.version + '-' + self.spec.source.release + + return self.destdir + self.const.tmp_dir_suffix \ + + '/' + packageDir + self.const.install_dir_suffix + + __instance = __impl() + + def __init__(self, pspecfile = None): + if pspecfile != None: + self.__instance.setSpecFile(pspecfile) + + def __getattr__(self, attr): + return getattr(self.__instance, attr) + + def __setattr__(self, attr, value): + return setattr(self.__instance, attr, value) + + +# create a default context WITH NO PSPEC +ctx = Context() diff --git a/lib/pisi/dependency.py b/lib/pisi/dependency.py new file mode 100644 index 00000000..9f1379f8 --- /dev/null +++ b/lib/pisi/dependency.py @@ -0,0 +1,3 @@ +# dependency analyzer +# maintainer: eray and caglar + diff --git a/lib/pisi/fetcher.py b/lib/pisi/fetcher.py new file mode 100644 index 00000000..79651ff6 --- /dev/null +++ b/lib/pisi/fetcher.py @@ -0,0 +1,139 @@ +# -*- coding: utf-8 -*- +# download magic +# maintainer: baris and meren + +# python standard library modules +import urlparse +import urllib2 +import os + +# pisi modules +import util + +class FetchError (Exception): + pass + +class Fetcher: + """Yet another Pisi tool for fetching files from various sources..""" + def __init__(self, ctx): + self.uri = ctx.spec.source.archiveUri + self.filedest = ctx.archives_dir() + util.check_dir(self.filedest) + self.scheme = "file" + self.netloc = "" + self.filepath = "" + self.filename = "" + self.percent = 0 + self.rate = 0.0 + self.percentHook = None + from string import split + u = urlparse.urlparse(self.uri) + self.scheme, self.netloc, self.filepath = u[0], u[1], u[2] + self.filename = os.path.basename(self.uri) + + def fetch (self): + """Return value: Fetched file's full path..""" + + if self.filename == "": + self.err("filename error") + + if os.access(self.filedest, os.W_OK) == False: + self.err("no perm to write to dest dir") + + scheme_err = lambda: self.err("unexpected scheme") + + handlers = { + 'file': self.fetchLocalFile, + 'http': self.fetchRemoteFile, + 'ftp' : self.fetchRemoteFile + }; handlers.get(self.scheme, scheme_err)() + + return self.filedest + "/" + self.filename + + def doGrab(self, file, dest, totalsize): + symbols = [' B/s', 'KB/s', 'MB/s', 'GB/s'] + from time import time + tt, oldsize = int(time()), 0 + p = Progress(totalsize) + bs, size = 1024, 0 + symbol, depth = "B/s", 0 + st = time() + chunk = file.read(bs) + size = size + len(chunk) + self.percent = p.update(size) + while chunk: + dest.write(chunk) + chunk = file.read(bs) + size = size + len(chunk) + ct = time() + if int(tt) != int(ct): + self.rate = size / (ct - st) + while self.rate > 1000 and depth < 3: + self.rate /= 1024 + depth += 1 + symbol, depth = symbols[depth], 0 + oldsize, tt = size, time() + if p.update(size): + self.percent = p.percent + if self.percentHook != None: + retval = {'filename': self.filename, + 'percent' : self.percent, + 'rate': self.rate, + 'symbol': symbol} + self.percentHook(retval) + + dest.close() + + + def fetchLocalFile (self): + from shutil import copyfile + + if os.access(self.filepath, os.F_OK) == False: + self.err("no such file or no perm to read") + + dest = open(self.filedest + "/" + self.filename , "w") + totalsize = os.path.getsize(self.filepath) + file = open(self.filepath) + self.doGrab(file, dest, totalsize) + + + def fetchRemoteFile (self): + from httplib import HTTPException + + try: + file = urllib2.urlopen(self.uri) + headers = file.info() + + except ValueError, e: + self.err('%s' % (e, )) + except IOError, e: + self.err('%s' % (e, )) + except OSError, e: + self.err('%s' % (e, )) + except HTTPException, e: + self.err(('(%s): %s') % (e.__class__.__name__, e)) + + if not headers is None and not headers.has_key('Content-Length'): + self.err('file not found') + else: totalsize = int(headers['Content-Length']) + + dest = open(self.filedest + "/" + self.filename , "w") + self.doGrab(file, dest, totalsize) + + + def err (self, error): + raise FetchError(error) + +class Progress: + def __init__(self, totalsize): + self.totalsize = totalsize + self.percent = 0 + + def update(self, size): + percent = (size * 100) / self.totalsize + if percent and self.percent is not percent: + self.percent = percent + return percent + else: + return 0 + diff --git a/lib/pisi/install.py b/lib/pisi/install.py new file mode 100644 index 00000000..fe98d925 --- /dev/null +++ b/lib/pisi/install.py @@ -0,0 +1,46 @@ +# -*- coding: utf-8 -*- + +#import package +from specfile import * +from package import Package +import util +from context import ctx +from ui import ui +import installdb +import packagedb +import dependency +#import conflicts + +class InstallError(Exception): + pass + +def install_package_file(package_fn): + + package = Package(package_fn, 'r') + # extract control files + util.clean_dir(ctx.install_dir()) + ui.info('extracting files\n') + package.extract_PISI_files(ctx.install_dir()) + + # verify package + # check if we have all required files + + metadata = MetaData() + metadata.read(ctx.install_dir() + '/metadata.xml') + # check package semantics + if not metadata.verify(): + raise InstallError("MetaData format wrong") + + # check file system requirements + # what to do if / is split into /usr, /var, etc.? + + # check conflicts + # check dependencies + + # unzip package in place + + # update databases + + # installdb + installdb.install(spec.spec.install_dir() + '/files.xml') + diff --git a/lib/pisi/installdb.py b/lib/pisi/installdb.py new file mode 100644 index 00000000..674af9b6 --- /dev/null +++ b/lib/pisi/installdb.py @@ -0,0 +1,52 @@ +# -*- coding: utf-8 -*- +# installation database +# maintainer: eray and caglar + +import os +import bsddb.dbshelve as shelve + +from context import ctx +import util + +util.check_dir(ctx.db_dir()) +d = shelve.open(ctx.db_dir() + '/install.bdb') +files_dir = ctx.archives_dir() + "/files" + +class InstallDBError(Exception): + pass + +def files_name(name, version, release): + return files_dir + '/' + name + '-' + version + '-' + release + +def files(n, v, r): + return file(files_name(n,v,r)) + +def is_recorded(name, version, release): + key = name + version + release + return d.has_key(key) + +def is_installed(name, version, release): + key = name + version + release + return is_recorded(name,version,release) and d[key]=='i' + +def is_removed(name, version, release): + key = name + version + release + return is_recorded(name,version,release) and d[key]=='r' + +def install(name, version, release, files_xml): + key = name + version + release + if is_installed(name, version, release): + raise InstallDBError("already installed") + d[key] = 'i' + util.copy_file(files_xml, files_name(name, version, release)) + +def remove(name, version, release): + key = name + version + release + d[key] = 'r' + +def purge(name, version, release): + os.unlink(files_name(name, version, release)) + key = name + version + release + del d[key] + + diff --git a/lib/pisi/oo.py b/lib/pisi/oo.py new file mode 100644 index 00000000..4cbbd866 --- /dev/null +++ b/lib/pisi/oo.py @@ -0,0 +1,20 @@ +# OO extensions +# thes are really cool, you can't do this in C++ :) + +class const: + "Constant members implementation" + class ConstError(TypeError): + pass + + def __setattr__(self, name, value): + if self.__dict__.has_key(name): + raise self.ConstError, "Can't rebind constant: %s" % name + # Binding an attribute once to a const is available + self.__dict__[name] = value + + def __delattr__(self, name): + if self.__dict__.has_key(name): + raise self.ConstError, "Can't unbind constant: %s" % name + # we don't have an attribute by this name + raise NameError, name + diff --git a/lib/pisi/package.py b/lib/pisi/package.py new file mode 100644 index 00000000..73e1e9a5 --- /dev/null +++ b/lib/pisi/package.py @@ -0,0 +1,30 @@ +# package abstraction +# provides methods to add/remove files, extract control files +# maintainer: baris and meren + +class Package: + """Package: PISI package class""" + def __init__(self, packagefn, mode): + self.filename = packagefn + self.mode = mode # bu gerekli mi? + # etc. etc. + + def add_file(self, fn): + """add a file to package""" + + def extract(self, outdir): + """extract entire package contents to directory""" + extract_dir('', outdir) # means package root + + def extract_file(self, path, outdir): + """extract file with path to outdir""" + + def extract_dir(self, dir, outdir): + """extract directory recursively""" + + def extract_PISI_files(self, outdir): + """extract PISI control files: metadata.xml, files.xml, + action scripts, etc.""" + self.extract_file('metadata.xml', outdir) + self.extract_file('files.xml', outdir) + self.extract_dir('Config', outdir) diff --git a/lib/pisi/packagedb.py b/lib/pisi/packagedb.py new file mode 100644 index 00000000..eccc26b1 --- /dev/null +++ b/lib/pisi/packagedb.py @@ -0,0 +1,28 @@ +# -*- coding: utf-8 -*- +# package database +# interface for update/query to local package repository +# maintainer: eray and caglar + +# we basically store everything in PackageInfo class +# yes, we are cheap + +import bsddb.dbshelve as shelve + +import util +from context import ctx + +util.check_dir(ctx.db_dir()) +d = shelve.open(ctx.db_dir() + '/package.bdb') + +def has_package(name): + return d.has_key(name) + +def get_package(name): + return d[name] + +def add_package(name, package_info): + d[name] = package_info + +def remove_package(name): + del d[name] + diff --git a/lib/pisi/sourcedb.py b/lib/pisi/sourcedb.py new file mode 100644 index 00000000..71af3727 --- /dev/null +++ b/lib/pisi/sourcedb.py @@ -0,0 +1,18 @@ +# -*- coding: utf-8 -*- +# package source database +# interface for update/query to local package repository +# maintainer: eray and caglar + +# we basically store everything in sourceinfo class +# yes, we are cheap + +import bsddb.dbshelve as shelve + +util.check_dir(config.db_dir()) +d = shelve.open(config.db_dir() + '/source.bdb') + +def add_source(name, source_info): + d[name] = source_info + +def remove_source(name): + del d[name] diff --git a/lib/pisi/specfile.py b/lib/pisi/specfile.py new file mode 100644 index 00000000..9566beca --- /dev/null +++ b/lib/pisi/specfile.py @@ -0,0 +1,105 @@ +# -*- coding: utf-8 -*- +# read/write PISI source package specification file + +import xml.dom.minidom +from xmlfile import * +from os.path import basename + +class PatchInfo: + def __init__(self, filenm, ctype): + self.filename = filenm + self.compressionType = ctype + + def __init__(self, node): + self.filename = getNodeText(node) + self.compressionType = getNodeAttribute(node, "compressionType") + +class DepInfo: + def __init__(self, node): + self.package = getNodeText(node).strip() + self.versionFrom = getNodeAttribute(node, "versionFrom") + +class HistoryInfo: + def __init__(self, node): + self.date = getNodeText(getNode(node, "Date")) + self.version = getNodeText(getNode(node, "Version")) + self.release = getNodeText(getNode(node, "Release")) + +class PathInfo: + def __init__(self, node): + self.pathname = getNodeText(node) + self.fileType = getNodeAttribute(node, "fileType") + +# a structure to hold source information +class SourceInfo: + pass + +class PackageInfo: + def __init__(self, node): + self.name = getNodeText(getNode(node, "Name")) + self.summary = getNodeText(getNode(node, "Summary")) + self.description = getNodeText(getNode(node, "Description")) + self.category = getNodeText(getNode(node, "Category")) + iDepElts = getAllNodes(node, "InstallDependencies") + self.installDeps = [DepInfo(x) for x in iDepElts] + rtDepElts = getAllNodes(node, "RuntimeDependencies") + self.runtimeDeps = [DepInfo(x) for x in rtDepElts] + self.paths = [PathInfo(x) for x in getAllNodes(node, "Files/Path")] + +class SpecFile(XmlFile): + """A class for reading/writing from/to a PSPEC (PISI SPEC) file.""" + + def __init__(self): + XmlFile.__init__(self,"PSPEC") + + def read(self, filename): + """Read PSPEC file""" + + self.readxml(filename) + + self.source = SourceInfo() + self.source.name = self.getChildText("Source/Name") + archiveNode = self.getNode("Source/Archive") + self.source.archiveUri = getNodeText(archiveNode).strip() + self.source.archiveName = basename(self.source.archiveUri) + self.source.archiveType = getNodeAttribute(archiveNode, "archType") + self.source.archiveMD5 = getNodeAttribute(archiveNode, "md5sum") + patchElts = self.getChildElts("Source/Patches") + if patchElts: + self.source.patches = [PatchInfo(p) for p in patchElts] + + buildDepElts = self.getChildElts("Source/BuildDependencies") + if buildDepElts: + self.source.buildDeps = [DepInfo(d) for d in buildDepElts] + + historyElts = self.getAllNodes("History/Update") + self.source.history = [HistoryInfo(x) for x in historyElts] + + # As we have no Source/Version tag we need to get + # the last version and release information + # from the first child of History/Update. And it works :) + self.source.version = self.source.history[0].version + self.source.release = self.source.history[0].release + + # find all binary packages + packageElts = self.getAllNodes("Package") + self.packages = [PackageInfo(p) for p in packageElts] + + def verify(self): + """Verify PSPEC structures, are they what we want of them?""" + return True + + def write(self, filename): + """Write PSPEC file""" + self.writexml(filename) + +class MetaData(SpecFile): + """This is a superset of the source spec definition""" + + def read(self, filename): + SpecFile.read(filename) + distribution = self.getNodeText("Source/Distribution") + distributionRelease = self.getNodeText("Source/DistributionRelease") + architecture = self.getNodeText("Source/Architecture") + installSize = self.getNodeText("Source/InstallSize") + diff --git a/lib/pisi/ui.py b/lib/pisi/ui.py new file mode 100644 index 00000000..163aae35 --- /dev/null +++ b/lib/pisi/ui.py @@ -0,0 +1,30 @@ +# generic user interface + +import sys +from colors import colorize + +def register(_impl): + """ Register a UI implementation""" + ui = _impl + +# default UI implementation +class CLI: + def __init__(self, debuggy = True): + self.showDebug = debuggy + + def info(self, msg): + sys.stdout.write(colorize(msg, 'blue')) + sys.stdout.flush() + + def debug(self, msg): + if showDebug: + sys.stdout.write(msg) + sys.stdout.flush() + + def error(self,msg): + sys.stdout.write(colorize(msg, 'red')) + sys.stdout.flush() + +# default UI is CLI +ui = CLI() + diff --git a/lib/pisi/util.py b/lib/pisi/util.py new file mode 100644 index 00000000..a26f8c76 --- /dev/null +++ b/lib/pisi/util.py @@ -0,0 +1,69 @@ +# -*- coding: utf-8 -*- +# misc. utility functions, including process and file utils +# maintainer: eray and caglar and baris and meren! + +import os +import sys +import md5 +from ui import ui + +class FileError(Exception): + pass + +class UtilError(Exception): + pass + +# shorthand to check if a file exists +def check_file(file, mode = os.F_OK): + if not os.access(file, mode): + raise FileError("File " + file + " not found") + +# check if directory exists, and create if it doesn't +# works recursively +# FIXME: could have a better name +def check_dir(dir): + dir = dir.strip().rstrip("/") + if not os.access(dir, os.F_OK): + os.makedirs(dir) + +def clean_dir(top): + """Remove all content of a directory (top)""" + for root, dirs, files in os.walk(top, topdown=False): + for name in files: + os.remove(os.path.join(root, name)) + for name in dirs: + os.rmdir(os.path.join(root, name)) + +def copy_file(s,d): + check_file(s) + check_dir(os.path.dirname(d)) + fs = file(s, 'rb') + fd = file(d, 'wb') + for l in fs: + fd.write(l) + +def copy_dir(): + raise UtilError("not implemented") + +def md5_file(filename): + m = md5.new() + f = file(filename, 'rb') + for l in f: + m.update(l) + return m.hexdigest() + +# run a command non-interactively +def run_batch(cmd): + ui.info('running ' + cmd) + a = os.popen(cmd) + lines = a.readlines() + ret = a.close() + ui.debug('return value ' + ret) + successful = ret == None + if not successful: + ui.error('ERROR: executing command: ' + cmd + '\n' + strlist(lines)) + return (successful,lines) + +# print a list +def strlist(l): + return string.join(map(lambda x: str(x) + ' ', l)) diff --git a/lib/pisi/xmlfile.py b/lib/pisi/xmlfile.py new file mode 100644 index 00000000..5dbef393 --- /dev/null +++ b/lib/pisi/xmlfile.py @@ -0,0 +1,141 @@ +# -*- coding: utf-8 -*- +# some helper functions for using minidom + +import xml.dom.minidom as mdom + +class XmlError(Exception): + pass + +# static functions + +def getNodeAttribute(node, attrname): + for i in range(node.attributes.length): + attr = node.attributes.item(i) + if attr.name == attrname: + return attr.childNodes[0].data + +def getNodeText(node): + # get the first child + try: + child = node.childNodes[0] + except IndexError: + return None + except AttributeError: # no node by that name + return None + if child.nodeType == child.TEXT_NODE: + return child.data + else: + raise XmlError("getNodeText: Expected text node, got something else!") + +def getChildText(node_s, tagpath): + node = getNode(node_s, tagpath) + if not node: + return None + return getNodeText(node) + +def getChildElts(node): + """get only child elements""" + return filter(lambda x:x.nodeType == x.ELEMENT_NODE, node.childNodes) + +def getNode(node, tagpath): + """returns the *first* matching node for given tag path.""" + + tags = tagpath.split('/') + + # iterative code to search for the path + + # get DOM for top node + nodeList = node.getElementsByTagName(tags[0]) + if len(nodeList) == 0: + return None # not found + + node = nodeList[0] # discard other matches + for tag in tags[1:]: + nodeList = node.getElementsByTagName(tag) + if len(nodeList) == 0: + return None + else: + node = nodeList[0] + + return node + +def getAllNodes(node, tags): + """retrieve all nodes that match a given tag path.""" + + if len(tags) == 0: + return [] + + nodeList = node.getElementsByTagName(tags[0]) + if len(nodeList) == 0: + return [] + + for tag in tags[1:]: + results = map(lambda x: x.getElementsByTagName(tag),nodeList) + nodeList = [] + for x in results: + nodeList.extend(x) + pass # emacs indentation error, keep it here + + if len(nodeList) == 0: + return [] + + return nodeList + + +# xmlfile class that further abstracts a dom object + +class XmlFile(object): + """A class for retrieving information from an XML file""" + + def __init__(self, rootTag): + self.rootTag = rootTag + + def readxml(self, fileName): + self.dom = mdom.parse(fileName) + + def writexml(self, fileName): + f = file(fileName,'w') + self.dom.writexml(f) + + def verifyRootTag(self): + if self.dom.documentElement.tagName != self.rootTag: + raise XmlError("Root tagname not " + self.rootTag + " as expected") + + def getNode(self, tagPath): + """returns the *first* matching node for given tag path.""" + self.verifyRootTag() + return getNode(self.dom.documentElement, tagPath) + + def getAllNodes(self, tagPath): + """returns all nodes matching a given tag path.""" + self.verifyRootTag() + tags = tagPath.split('/') + return getAllNodes(self.dom.documentElement, tags) + + def getChildren(self, tagpath): + """ returns the children of the given path""" + node = self.getNode(tagpath) + return node.childNodes + + # get only elements of a given type + # BUG: this doesn't work + def getChildrenWithType(self, tagpath, type): + """ returns the children of the given path, only with given type """ + node = self.getNode(tagpath) + return filter(lambda x:x.nodeType == type, node.childNodes) + + # get only child elements + def getChildElts(self, tagpath): + """ returns the children of the given path, only with given type """ + node = self.getNode(tagpath) + try: + return filter(lambda x:x.nodeType == x.ELEMENT_NODE, node.childNodes) + except AttributeError: + return None + + def getChildText(self, tagpath): + node = self.getNode(tagpath) + if not node: + return None + return getNodeText(node) +