[jhbuild/wip/packaging: 2/6] autotools: Use make install DESTDIR=



commit 01a5af5ffe25c99b7f7bd9e441ee03c3490e9256
Author: Colin Walters <walters verbum org>
Date:   Mon Apr 25 20:36:47 2011 -0400

    autotools: Use make install DESTDIR=
    
    Like most "package managers", use DESTDIR= to create a temporary
    installation tree, from which we can generate a file manifest.
    
    Add a <manifest> node to PackageDB which contains the contents of a
    build.
    
    Then implement "uninstall" using this.  A big advantage now is that we
    don't need to do a build in order to do an uninstall, and uninstall is
    independent of version.
    
    https://bugzilla.gnome.org/show_bug.cgi?id=647231

 jhbuild/modtypes/__init__.py  |   52 ++++++++++++
 jhbuild/modtypes/autotools.py |   21 +++---
 jhbuild/utils/Makefile.am     |    1 +
 jhbuild/utils/fileutils.py    |   47 +++++++++++
 jhbuild/utils/packagedb.py    |  172 +++++++++++++++++++++++++++++++++++-----
 5 files changed, 261 insertions(+), 32 deletions(-)
---
diff --git a/jhbuild/modtypes/__init__.py b/jhbuild/modtypes/__init__.py
index 6b27eeb..9a511dc 100644
--- a/jhbuild/modtypes/__init__.py
+++ b/jhbuild/modtypes/__init__.py
@@ -28,10 +28,12 @@ __all__ = [
     ]
 
 import os
+import shutil
 
 from jhbuild.errors import FatalError, CommandError, BuildStateError, \
              SkipToEnd, UndefinedRepositoryError
 from jhbuild.utils.sxml import sxml
+from jhbuild.utils.fileutils import move_directory_tree
 
 _module_types = {}
 def register_module_type(name, parse_func):
@@ -131,6 +133,7 @@ class Package:
         self.suggests = suggests
         self.tags = []
         self.moduleset_name = None
+        self.supports_install_destdir = False
 
     def __repr__(self):
         return "<%s '%s'>" % (self.__class__.__name__, self.name)
@@ -144,6 +147,55 @@ class Package:
     def get_builddir(self, buildscript):
         raise NotImplementedError
 
+    def _get_destdir(self, buildscript):
+        return os.path.join(buildscript.config.workdir, 'root-%s' % (self.name, ))
+
+    def prepare_installroot(self, buildscript):
+        assert self.supports_install_destdir
+        """Return a directory suitable for use as e.g. DESTDIR with "make install"."""
+        destdir = self._get_destdir(buildscript)
+        if os.path.exists(destdir):
+            shutil.rmtree(destdir)
+        os.makedirs(destdir)
+        return destdir
+
+    def _process_install_files(self, installroot, curdir, prefix):
+        """Strip the prefix from all files in the install root, and move
+them into the prefix."""
+        assert os.path.isdir(installroot) and os.path.isabs(installroot)
+        assert os.path.isdir(curdir) and os.path.isabs(curdir)
+        assert os.path.isdir(prefix) and os.path.isabs(prefix)
+
+        if prefix.endswith('/'):
+            prefix = prefix[:-1]
+
+        names = os.listdir(curdir)
+        for filename in names:
+            src_path = os.path.join(curdir, filename)
+            assert src_path.startswith(installroot)
+            dest_path = src_path[len(installroot):]
+            if os.path.isdir(src_path):
+                if os.path.exists(dest_path):
+                    if not os.path.isdir(dest_path):
+                        os.unlink(dest_path)
+                        os.mkdir(dest_path)
+                else:
+                    os.mkdir(dest_path)
+                self._process_install_files(installroot, src_path, prefix)
+                os.rmdir(src_path)
+            else:
+                os.rename(src_path, dest_path)
+
+    def process_install(self, buildscript, revision):
+        assert self.supports_install_destdir
+        destdir = self._get_destdir(buildscript)
+        buildscript.packagedb.add(self.name, revision or '', destdir)
+        self._process_install_files(destdir, destdir, buildscript.config.prefix)
+        try:
+            os.rmdir(destdir)
+        except:
+            pass
+
     def get_revision(self):
         return self.branch.tree_id()
 
diff --git a/jhbuild/modtypes/autotools.py b/jhbuild/modtypes/autotools.py
index 215df91..f9c576f 100644
--- a/jhbuild/modtypes/autotools.py
+++ b/jhbuild/modtypes/autotools.py
@@ -66,6 +66,7 @@ class AutogenModule(Package, DownloadableModule):
         self.makefile = makefile
         self.autogen_template = autogen_template
         self.check_target = check_target
+        self.supports_install_destdir = True
 
     def get_srcdir(self, buildscript):
         return self.branch.srcdir
@@ -262,14 +263,18 @@ class AutogenModule(Package, DownloadableModule):
 
     def do_install(self, buildscript):
         buildscript.set_action(_('Installing'), self)
+        destdir = self.prepare_installroot(buildscript)
         if self.makeinstallargs:
-            cmd = '%s %s' % (os.environ.get('MAKE', 'make'), self.makeinstallargs)
+            cmd = '%s %s DESTDIR=%s' % (os.environ.get('MAKE', 'make'),
+                                        self.makeinstallargs,
+                                        destdir)
         else:
-            cmd = '%s install' % os.environ.get('MAKE', 'make')
-
+            cmd = '%s install DESTDIR=%s' % (os.environ.get('MAKE', 'make'),
+                                             destdir)
         buildscript.execute(cmd, cwd = self.get_builddir(buildscript),
                     extra_env = self.extra_env)
-        buildscript.packagedb.add(self.name, self.get_revision() or '')
+        self.process_install(buildscript, self.get_revision())
+
     do_install.depends = [PHASE_BUILD]
 
     def do_distclean(self, buildscript):
@@ -291,12 +296,8 @@ class AutogenModule(Package, DownloadableModule):
 
     def do_uninstall(self, buildscript):
         buildscript.set_action(_('Uninstalling'), self)
-        makeargs = self.makeargs + ' ' + self.config.module_makeargs.get(
-                self.name, self.config.makeargs)
-        cmd = '%s %s uninstall' % (os.environ.get('MAKE', 'make'), makeargs)
-        buildscript.execute(cmd, cwd = self.get_builddir(buildscript),
-                extra_env = self.extra_env)
-        buildscript.packagedb.remove(self.name)
+        # Since we are supports_install_destdir = True, just delegate to packagedb
+        buildscript.packagedb.uninstall(self.name, buildscript)
 
     def xml_tag_and_attrs(self):
         return ('autotools',
diff --git a/jhbuild/utils/Makefile.am b/jhbuild/utils/Makefile.am
index b6d63bf..a404892 100644
--- a/jhbuild/utils/Makefile.am
+++ b/jhbuild/utils/Makefile.am
@@ -3,6 +3,7 @@ appdir = $(pythondir)/jhbuild/utils/
 app_PYTHON = \
 	__init__.py \
 	cmds.py \
+	fileutils.py \
 	httpcache.py \
 	notify.py \
 	packagedb.py \
diff --git a/jhbuild/utils/fileutils.py b/jhbuild/utils/fileutils.py
new file mode 100644
index 0000000..adcb240
--- /dev/null
+++ b/jhbuild/utils/fileutils.py
@@ -0,0 +1,47 @@
+# jhbuild - a build script for GNOME 1.x and 2.x
+# Copyright (C) 2011  Red Hat, Inc.
+#
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation; either version 2 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+
+import sys
+import os
+
+def _move_directory_tree_recurse(source, target):
+    assert os.path.isdir(source) and os.path.isabs(source)
+    assert os.path.isabs(target)
+    # We don't require here that the destination path exist
+    if os.path.exists(target):
+        if not os.path.isdir(target):
+            os.unlink(target)
+            os.mkdir(target)
+    else:
+        os.mkdir(target)
+
+    names = os.listdir(source)
+    for filename in names:
+        source_path = os.path.join(source, filename)
+        dest_path = os.path.join(target, filename)
+        if os.path.isdir(source_path):
+            _move_directory_tree_recurse(source_path, dest_path)
+            os.rmdir(source_path)
+        else:
+            os.rename(source_path, dest_path)
+
+def move_directory_tree(source, target):
+    """Move all of the files from TARGET into SOURCE."""
+    assert os.path.isdir(source) and os.path.isabs(source)
+    assert os.path.isdir(target) and os.path.isabs(target)
+    
+    _move_directory_tree_recurse(source, target)
diff --git a/jhbuild/utils/packagedb.py b/jhbuild/utils/packagedb.py
index 09d4b8b..d26c7e7 100644
--- a/jhbuild/utils/packagedb.py
+++ b/jhbuild/utils/packagedb.py
@@ -17,6 +17,7 @@
 # along with this program; if not, write to the Free Software
 # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
 
+import os
 import time
 try:
     import xml.dom.minidom
@@ -32,6 +33,80 @@ def _parse_isotime(string):
 def _format_isotime(tm):
     return time.strftime('%Y-%m-%dT%H:%M:%SZ', time.gmtime(tm))
 
+def _get_text_content(node):
+    content = ''
+    for child in node.childNodes:
+        if (child.nodeType == child.TEXT_NODE):
+            content += child.nodeValue
+    return content
+
+def _list_from_xml(node, child_name):
+    """Parse XML like:
+        <foolist>
+            <item>content</item>
+            <item>more content</item>
+            ...
+        </foolist>."""
+    result = []
+    for child in node.childNodes:
+        if not (child.nodeType == child.ELEMENT_NODE and child.nodeName == child_name):
+            continue
+        result.append(_get_text_content(child))
+    return result
+
+def _find_node(node, child_name):
+    """Get the first child node named @child_name"""
+    for child in node.childNodes:
+        if not (child.nodeType == child.ELEMENT_NODE and child.nodeName == child_name):
+            continue
+        return child
+    return None
+
+class PackageEntry:
+    def __init__(self, package, version, manifest,
+                 metadata):
+        self.package = package # string
+        self.version = version # string
+        self.manifest = manifest # list of strings
+        self.metadata = metadata # hash of string to value
+
+    @classmethod
+    def from_xml(cls, node):
+        package = node.getAttribute('package')
+        version = node.getAttribute('version')
+        metadata = {}
+
+        installed_string = node.getAttribute('installed')
+        if installed_string:
+            metadata['installed-date'] = _parse_isotime(installed_string)
+
+        manifestNode = _find_node(node, 'manifest')
+        if manifestNode:
+            manifest = _list_from_xml(manifestNode, 'file')
+        else:
+            manifest = None
+        return cls(package, version, manifest, metadata)
+
+    def to_xml(self, document):
+        entryNode = document.createElement('entry')
+        entryNode.setAttribute('package', self.package)
+        entryNode.setAttribute('version', self.version)
+        if 'installed-date' in self.metadata:
+            entryNode.setAttribute('installed', _format_isotime(self.metadata['installed-date']))
+        entryNode.appendChild(document.createTextNode('\n'))
+        if self.manifest is not None:
+            manifestNode = document.createElement('manifest')
+            entryNode.appendChild(manifestNode)
+            manifestNode.appendChild(document.createTextNode('\n'))
+            for filename in self.manifest:
+                node = document.createElement('file')
+                node.appendChild(document.createTextNode(filename))
+                manifestNode.appendChild(document.createTextNode('  '))
+                manifestNode.appendChild(node)
+                manifestNode.appendChild(document.createTextNode('\n'))
+            entryNode.appendChild(document.createTextNode('\n'))
+        return entryNode
+
 class PackageDB:
     def __init__(self, dbfile):
         self.dbfile = dbfile
@@ -49,10 +124,9 @@ class PackageDB:
         for node in document.documentElement.childNodes:
             if node.nodeType != node.ELEMENT_NODE: continue
             if node.nodeName != 'entry': continue
-            package = node.getAttribute('package')
-            version = node.getAttribute('version')
-            installed = _parse_isotime(node.getAttribute('installed'))
-            self.entries[package] = (version, installed)
+            
+            entry = PackageEntry.from_xml(node)
+            self.entries[entry.package] = entry
         document.unlink()
 
     def _write_cache(self):
@@ -60,44 +134,98 @@ class PackageDB:
         document.appendChild(document.createElement('packagedb'))
         node = document.createTextNode('\n')
         document.documentElement.appendChild(node)
-        for package in self.entries:
-            version, installed = self.entries[package]
-            node = document.createElement('entry')
-            node.setAttribute('package', package)
-            node.setAttribute('version', version)
-            node.setAttribute('installed', _format_isotime(installed))
+        for package,entry in self.entries.iteritems():
+            node = entry.to_xml(document)
             document.documentElement.appendChild(node)
-
             node = document.createTextNode('\n')
             document.documentElement.appendChild(node)
 
-        document.writexml(open(self.dbfile, 'w'))
+        tmp_dbfile_path = self.dbfile + '.tmp'
+        tmp_dbfile = open(tmp_dbfile_path, 'w')
+        try:
+            document.writexml(tmp_dbfile)
+        except:
+            tmp_dbfile.close()
+            os.unlink(tmp_dbfile_path)
+            raise
+        tmp_dbfile.close()
+        os.rename(tmp_dbfile_path, self.dbfile)
         document.unlink()
 
-    def add(self, package, version):
+    def _accumulate_dirtree_contents_recurse(self, path, contents):
+        assert os.path.isdir(path)
+        names = os.listdir(path)
+        for name in names:
+            subpath = os.path.join(path, name)
+            if os.path.isdir(subpath):
+                contents.append(subpath + '/')
+                self._accumulate_dirtree_contents_recurse(subpath, contents)
+            else:
+                contents.append(subpath)
+
+    def _accumulate_dirtree_contents(self, path):
+        contents = []
+        self._accumulate_dirtree_contents_recurse(path, contents)
+        if not path.endswith('/'):
+            path = path + '/'
+        pathlen = len(path)
+        for i,subpath in enumerate(contents):
+            assert subpath.startswith(path)
+            # Strip the temporary prefix, then make it absolute again for our target
+            contents[i] = '/' + subpath[pathlen:]
+        return contents
+
+    def add(self, package, version, destdir):
         '''Add a module to the install cache.'''
         now = time.time()
-        self.entries[package] = (version, now)
+        contents = self._accumulate_dirtree_contents(destdir)
+        metadata = {'installed-date': now}
+        self.entries[package] = PackageEntry(package, version, contents, metadata)
         self._write_cache()
 
     def check(self, package, version=None):
         '''Check whether a particular module is installed.'''
         if not self.entries.has_key(package): return False
-        p_version, p_installed = self.entries[package]
+        entry = self.entries[package]
         if version is not None:
-            if version != p_version: return False
+            if entry.version != version: return False
         return True
 
     def installdate(self, package, version=None):
         '''Get the install date for a particular module.'''
         if not self.entries.has_key(package): return None
-        p_version, p_installed = self.entries[package]
+        entry = self.entries[package]
         if version:
-            if version != p_version: return None
-        return p_installed
+            if entry.version != version: return None
+        return entry.metadata['installed-date']
 
-    def remove(self, package):
+    def uninstall(self, package_name, buildscript):
         '''Remove a module from the install cache.'''
-        if self.entries.has_key(package):
-            del self.entries[package]
+        if package_name in self.entries:
+            entry = self.entries[package_name]
+            if entry.manifest is None:
+                buildscript.message("warning: no manifest known for '%s', not deleting files")
+            else:
+                directories = []
+                for path in entry.manifest:
+                    assert os.path.isabs(path)
+                    print "Deleting %r" % (path, )
+                    if os.path.isdir(path):
+                        directories.append(path)
+                    else:
+                        os.unlink(path)
+                for directory in directories:
+                    if not directory.startswith(buildscript.prefix):
+                        # Skip non-prefix directories; otherwise we
+                        # may try to remove the user's ~ or something
+                        # (presumably we'd fail, but better not to try)
+                        continue
+                    try:
+                        os.rmdir(directory)
+                    except OSError, e:
+                        # Allow multiple components to use directories
+                        pass
+            del self.entries[package_name]
             self._write_cache()
+        else:
+            buildscript.message("warning: no package known for '%s'")



[Date Prev][Date Next]   [Thread Prev][Thread Next]   [Thread Index] [Date Index] [Author Index]