~ubuntu-branches/debian/sid/bzr-builddeb/sid

« back to all changes in this revision

Viewing changes to upstream/pristinetar.py

  • Committer: Bazaar Package Importer
  • Author(s): Jelmer Vernooij, Jelmer Vernooij, Jonathan Riddell, Scott Kitterman
  • Date: 2011-07-15 12:15:22 UTC
  • Revision ID: james.westby@ubuntu.com-20110715121522-avtc0uc3uuzcg7zn
Tags: 2.7.5
[ Jelmer Vernooij ]
* New 'bzr dep3-patch' subcommand that can generate DEP-3 compliant
  patches. LP: #460576

[ Jonathan Riddell ]
* Use new set_commit_message() hook in bzr to set the commit
  message from debian/changelog and set fixed bugs in tags. LP: #707274

[ Jelmer Vernooij ]
* Add dependency on devscripts >= 2.10.59, required now that 'dch --
  package' is used. LP: #783122
* Fix support for native packages with dashes in their version in
  sources.list. LP: #796853
* Fix deprecation warnings for TestCase.failUnlessExists and
  TestCase.failIfExists in bzr 2.4.

[ Scott Kitterman ]
* Delete debian/bzr-builddeb.dirs so the long obsolete and empty
  /usr/lib/python2.4/site-packages/bzrlib/plugins/bzr-builddeb/ is no
  longer created. Closes: #631564

[ Jelmer Vernooij ]
* Add support for xz and lzma tarballs. LP: #553668
* When importing upstream component tarballs, don't repack bz2/lzma
  tarballs to gz if the package is in v3 source format. LP: #810531

Show diffs side-by-side

added added

removed removed

Lines of Context:
 
1
#    pristinetar.py -- Providers of upstream source
 
2
#    Copyright (C) 2009-2011 Canonical Ltd.
 
3
#    Copyright (C) 2009 Jelmer Vernooij <jelmer@debian.org>
 
4
#
 
5
#    This file is part of bzr-builddeb.
 
6
#
 
7
#    bzr-builddeb is free software; you can redistribute it and/or modify
 
8
#    it under the terms of the GNU General Public License as published by
 
9
#    the Free Software Foundation; either version 2 of the License, or
 
10
#    (at your option) any later version.
 
11
#
 
12
#    bzr-builddeb is distributed in the hope that it will be useful,
 
13
#    but WITHOUT ANY WARRANTY; without even the implied warranty of
 
14
#    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 
15
#    GNU General Public License for more details.
 
16
#
 
17
#    You should have received a copy of the GNU General Public License
 
18
#    along with bzr-builddeb; if not, write to the Free Software
 
19
#    Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA  02110-1301  USA
 
20
 
 
21
from base64 import (
 
22
    standard_b64decode,
 
23
    standard_b64encode,
 
24
    )
 
25
import errno
 
26
import os
 
27
import shutil
 
28
import subprocess
 
29
import tempfile
 
30
 
 
31
from bzrlib.plugins.builddeb.errors import (
 
32
    MultipleUpstreamTarballsNotSupported,
 
33
    PackageVersionNotPresent,
 
34
    PerFileTimestampsNotSupported,
 
35
    )
 
36
from bzrlib.plugins.builddeb.upstream import UpstreamSource
 
37
from bzrlib.plugins.builddeb.util import (
 
38
    export,
 
39
    subprocess_setup,
 
40
    )
 
41
 
 
42
from bzrlib import osutils
 
43
from bzrlib.errors import (
 
44
    BzrError,
 
45
    NoSuchRevision,
 
46
    NoSuchTag,
 
47
    )
 
48
from bzrlib.trace import (
 
49
    note,
 
50
    warning,
 
51
    )
 
52
 
 
53
 
 
54
class PristineTarError(BzrError):
 
55
    _fmt = 'There was an error using pristine-tar: %(error)s.'
 
56
 
 
57
    def __init__(self, error):
 
58
        BzrError.__init__(self, error=error)
 
59
 
 
60
 
 
61
def reconstruct_pristine_tar(dest, delta, dest_filename):
 
62
    """Reconstruct a pristine tarball from a directory and a delta.
 
63
 
 
64
    :param dest: Directory to pack
 
65
    :param delta: pristine-tar delta
 
66
    :param dest_filename: Destination filename
 
67
    """
 
68
    command = ["pristine-tar", "gentar", "-",
 
69
               os.path.abspath(dest_filename)]
 
70
    try:
 
71
        proc = subprocess.Popen(command, stdin=subprocess.PIPE,
 
72
                cwd=dest, preexec_fn=subprocess_setup,
 
73
                stdout=subprocess.PIPE, stderr=subprocess.STDOUT)
 
74
    except OSError, e:
 
75
        if e.errno == errno.ENOENT:
 
76
            raise PristineTarError("pristine-tar is not installed")
 
77
        else:
 
78
            raise
 
79
    (stdout, stderr) = proc.communicate(delta)
 
80
    if proc.returncode != 0:
 
81
        raise PristineTarError("Generating tar from delta failed: %s" % stdout)
 
82
 
 
83
 
 
84
def make_pristine_tar_delta(dest, tarball_path):
 
85
    """Create a pristine-tar delta for a tarball.
 
86
 
 
87
    :param dest: Directory to generate pristine tar delta for
 
88
    :param tarball_path: Path to the tarball
 
89
    :return: pristine-tarball
 
90
    """
 
91
    # If tarball_path is relative, the cwd=dest parameter to Popen will make
 
92
    # pristine-tar faaaail. pristine-tar doesn't use the VFS either, so we
 
93
    # assume local paths.
 
94
    tarball_path = osutils.abspath(tarball_path)
 
95
    command = ["pristine-tar", "gendelta", tarball_path, "-"]
 
96
    try:
 
97
        proc = subprocess.Popen(command, stdout=subprocess.PIPE,
 
98
                cwd=dest, preexec_fn=subprocess_setup,
 
99
                stderr=subprocess.PIPE)
 
100
    except OSError, e:
 
101
        if e.errno == errno.ENOENT:
 
102
            raise PristineTarError("pristine-tar is not installed")
 
103
        else:
 
104
            raise
 
105
    (stdout, stderr) = proc.communicate()
 
106
    if proc.returncode != 0:
 
107
        raise PristineTarError("Generating delta from tar failed: %s" % stderr)
 
108
    return stdout
 
109
 
 
110
 
 
111
class PristineTarSource(UpstreamSource):
 
112
    """Source that uses the pristine-tar revisions in the packaging branch."""
 
113
 
 
114
    def __init__(self, tree, branch):
 
115
        self.branch = branch
 
116
        self.tree = tree
 
117
 
 
118
    def __repr__(self):
 
119
        return "<%s at %s>" % (self.__class__.__name__, self.branch.base)
 
120
 
 
121
    def tag_name(self, version, component=None, distro=None):
 
122
        """Gets the tag name for the upstream part of version.
 
123
 
 
124
        :param version: the Version object to extract the upstream
 
125
            part of the version number from.
 
126
        :param component: Name of the component (None for base)
 
127
        :param distro: Optional distribution name
 
128
        :return: a String with the name of the tag.
 
129
        """
 
130
        assert isinstance(version, str)
 
131
        if distro is None:
 
132
            name = "upstream-" + version
 
133
        else:
 
134
            name = "upstream-%s-%s" % (distro, version)
 
135
        if component is not None:
 
136
            name += "/%s" % component
 
137
        return name
 
138
 
 
139
    def tag_version(self, version, revid, component=None):
 
140
        """Tags the upstream branch's last revision with an upstream version.
 
141
 
 
142
        Sets a tag on the last revision of the upstream branch and on the main
 
143
        branch with a tag that refers to the upstream part of the version
 
144
        provided.
 
145
 
 
146
        :param version: the upstream part of the version number to derive the 
 
147
            tag name from.
 
148
        :param component: name of the component that is being imported
 
149
            (None for base)
 
150
        :param revid: the revid to associate the tag with, or None for the
 
151
            tip of self.pristine_upstream_branch.
 
152
        :return The tag name, revid of the added tag.
 
153
        """
 
154
        assert isinstance(version, str)
 
155
        tag_name = self.tag_name(version, component=component)
 
156
        self.branch.tags.set_tag(tag_name, revid)
 
157
        return tag_name, revid
 
158
 
 
159
    def import_tarballs(self, package, version, tree, parent_ids, tarballs,
 
160
            timestamp=None, author=None):
 
161
        """Import the upstream tarballs.
 
162
 
 
163
        :param package: Package name
 
164
        :param version: Package version
 
165
        :param path: Path with tree to import
 
166
        :param parent_ids: Parent revisions
 
167
        :param tarballs: List of (path, component, md5)
 
168
        :param timestamp: Optional timestamp for new commits
 
169
        :param author: Optional author for new commits
 
170
        :return: List of tuples with (component, tag, revid)
 
171
        """
 
172
        ret = []
 
173
        for (tarball, component, md5) in tarballs:
 
174
            (tag, revid) = self.import_component_tarball(
 
175
                package, version, tree, parent_ids, component,
 
176
                md5, tarball, author=author, timestamp=timestamp)
 
177
            ret.append((component, tag, revid))
 
178
        return ret
 
179
 
 
180
    def import_component_tarball(self, package, version, tree, parent_ids,
 
181
            component=None, md5=None, tarball=None, author=None, timestamp=None):
 
182
        """Import a tarball.
 
183
 
 
184
        :param package: Package name
 
185
        :param version: Upstream version
 
186
        :param component: Component name (None for base)
 
187
        """
 
188
        if component is not None:
 
189
            raise BzrError("Importing non-base tarballs not yet supported")
 
190
        tree.set_parent_ids(parent_ids)
 
191
        revprops = {}
 
192
        if md5 is not None:
 
193
            revprops["deb-md5"] = md5
 
194
            delta = self.make_pristine_tar_delta(tree, tarball)
 
195
            uuencoded = standard_b64encode(delta)
 
196
            if tarball.endswith(".tar.bz2"):
 
197
                revprops["deb-pristine-delta-bz2"] = uuencoded
 
198
            elif tarball.endswith(".tar.lzma"):
 
199
                revprops["deb-pristine-delta-lzma"] = uuencoded
 
200
            else:
 
201
                revprops["deb-pristine-delta"] = uuencoded
 
202
        if author is not None:
 
203
            revprops['authors'] = author
 
204
        timezone = None
 
205
        if timestamp is not None:
 
206
            timezone = timestamp[1]
 
207
            timestamp = timestamp[0]
 
208
        message = "Import upstream version %s" % (version,)
 
209
        if component is not None:
 
210
            message += ", component %s" % component
 
211
        revid = tree.commit(message, revprops=revprops, timestamp=timestamp,
 
212
            timezone=timezone)
 
213
        tag_name, _ = self.tag_version(version, revid=revid)
 
214
        return tag_name, revid
 
215
 
 
216
    def fetch_component_tarball(self, package, version, component, target_dir):
 
217
        revid = self.version_component_as_revision(package, version, component)
 
218
        try:
 
219
            rev = self.branch.repository.get_revision(revid)
 
220
        except NoSuchRevision:
 
221
            raise PackageVersionNotPresent(package, version, self)
 
222
        note("Using pristine-tar to reconstruct the needed tarball.")
 
223
        if self.has_pristine_tar_delta(rev):
 
224
            format = self.pristine_tar_format(rev)
 
225
        else:
 
226
            format = 'gz'
 
227
        target_filename = self._tarball_path(package, version, component,
 
228
                                             target_dir, format=format)
 
229
        try:
 
230
            self.reconstruct_pristine_tar(revid, package, version, target_filename)
 
231
        except PristineTarError:
 
232
            raise PackageVersionNotPresent(package, version, self)
 
233
        except PerFileTimestampsNotSupported:
 
234
            raise PackageVersionNotPresent(package, version, self)
 
235
        return target_filename
 
236
 
 
237
    def fetch_tarballs(self, package, version, target_dir):
 
238
        return [self.fetch_component_tarball(package, version, None, target_dir)]
 
239
 
 
240
    def _has_revision(self, revid, md5=None):
 
241
        self.branch.lock_read()
 
242
        try:
 
243
            graph = self.branch.repository.get_graph()
 
244
            if not graph.is_ancestor(revid, self.branch.last_revision()):
 
245
                return False
 
246
        finally:
 
247
            self.branch.unlock()
 
248
        if md5 is None:
 
249
            return True
 
250
        rev = self.branch.repository.get_revision(revid)
 
251
        try:
 
252
            return rev.properties['deb-md5'] == md5
 
253
        except KeyError:
 
254
            warning("tag present in branch, but there is no "
 
255
                "associated 'deb-md5' property in associated "
 
256
                "revision %s", revid)
 
257
            return True
 
258
 
 
259
    def version_as_revision(self, package, version, tarballs=None):
 
260
        if tarballs is None:
 
261
            return self.version_component_as_revision(package, version, component=None)
 
262
        elif len(tarballs) > 1:
 
263
            raise MultipleUpstreamTarballsNotSupported()
 
264
        else:
 
265
            return self.version_component_as_revision(package, version, tarballs[0][1],
 
266
                tarballs[0][2])
 
267
 
 
268
    def version_component_as_revision(self, package, version, component, md5=None):
 
269
        assert isinstance(version, str)
 
270
        for tag_name in self.possible_tag_names(version, component=component):
 
271
            try:
 
272
                revid = self.branch.tags.lookup_tag(tag_name)
 
273
            except NoSuchTag:
 
274
                continue
 
275
            else:
 
276
                if self._has_revision(revid, md5=md5):
 
277
                    return revid
 
278
        tag_name = self.tag_name(version, component=component)
 
279
        try:
 
280
            return self.branch.tags.lookup_tag(tag_name)
 
281
        except NoSuchTag:
 
282
            raise PackageVersionNotPresent(package, version, self)
 
283
 
 
284
    def has_version(self, package, version, tarballs=None):
 
285
        if tarballs is None:
 
286
            return self.has_version_component(package, version, component=None)
 
287
        elif len(tarballs) > 1:
 
288
            raise MultipleUpstreamTarballsNotSupported()
 
289
        else:
 
290
            return self.has_version_component(package, version, tarballs[0][1],
 
291
                    tarballs[0][2])
 
292
 
 
293
    def has_version_component(self, package, version, component, md5=None):
 
294
        assert isinstance(version, str), str(type(version))
 
295
        for tag_name in self.possible_tag_names(version, component=component):
 
296
            try:
 
297
                revid = self.branch.tags.lookup_tag(tag_name)
 
298
            except NoSuchTag:
 
299
                continue
 
300
            else:
 
301
                if self._has_revision(revid, md5=md5):
 
302
                    return True
 
303
        return False
 
304
 
 
305
    def possible_tag_names(self, version, component):
 
306
        assert isinstance(version, str)
 
307
        tags = [self.tag_name(version, component=component),
 
308
                self.tag_name(version, component=component, distro="debian"),
 
309
                self.tag_name(version, component=component, distro="ubuntu"),
 
310
                ]
 
311
        if component is None:
 
312
            # compatibility with git-buildpackage
 
313
            tags += ["upstream/%s" % version]
 
314
        return tags
 
315
 
 
316
    def has_pristine_tar_delta(self, rev):
 
317
        return ('deb-pristine-delta' in rev.properties
 
318
                or 'deb-pristine-delta-bz2' in rev.properties
 
319
                or 'deb-pristine-delta-lzma' in rev.properties)
 
320
 
 
321
    def pristine_tar_format(self, rev):
 
322
        if 'deb-pristine-delta' in rev.properties:
 
323
            return 'gz'
 
324
        elif 'deb-pristine-delta-bz2' in rev.properties:
 
325
            return 'bz2'
 
326
        elif 'deb-pristine-delta-lzma' in rev.properties:
 
327
            return 'lzma'
 
328
        assert self.has_pristine_tar_delta(rev)
 
329
        raise AssertionError("Not handled new delta type in "
 
330
                "pristine_tar_format")
 
331
 
 
332
    def pristine_tar_delta(self, rev):
 
333
        if 'deb-pristine-delta' in rev.properties:
 
334
            uuencoded = rev.properties['deb-pristine-delta']
 
335
        elif 'deb-pristine-delta-bz2' in rev.properties:
 
336
            uuencoded = rev.properties['deb-pristine-delta-bz2']
 
337
        elif 'deb-pristine-delta-lzma' in rev.properties:
 
338
            uuencoded = rev.properties['deb-pristine-delta-lzma']
 
339
        else:
 
340
            assert self.has_pristine_tar_delta(rev)
 
341
            raise AssertionError("Not handled new delta type in "
 
342
                    "pristine_tar_delta")
 
343
        return standard_b64decode(uuencoded)
 
344
 
 
345
    def reconstruct_pristine_tar(self, revid, package, version,
 
346
            dest_filename):
 
347
        """Reconstruct a pristine-tar tarball from a bzr revision."""
 
348
        tree = self.branch.repository.revision_tree(revid)
 
349
        tmpdir = tempfile.mkdtemp(prefix="builddeb-pristine-")
 
350
        try:
 
351
            dest = os.path.join(tmpdir, "orig")
 
352
            rev = self.branch.repository.get_revision(revid)
 
353
            if self.has_pristine_tar_delta(rev):
 
354
                export(tree, dest, format='dir')
 
355
                delta = self.pristine_tar_delta(rev)
 
356
                reconstruct_pristine_tar(dest, delta, dest_filename)
 
357
            else:
 
358
                export(tree, dest_filename, require_per_file_timestamps=True)
 
359
        finally:
 
360
            shutil.rmtree(tmpdir)
 
361
 
 
362
    def make_pristine_tar_delta(self, tree, tarball_path):
 
363
        tmpdir = tempfile.mkdtemp(prefix="builddeb-pristine-")
 
364
        try:
 
365
            dest = os.path.join(tmpdir, "orig")
 
366
            tree.lock_read()
 
367
            try:
 
368
                for (dp, ie) in tree.inventory.iter_entries():
 
369
                    ie._read_tree_state(dp, tree)
 
370
                export(tree, dest, format='dir')
 
371
            finally:
 
372
                tree.unlock()
 
373
            return make_pristine_tar_delta(dest, tarball_path)
 
374
        finally:
 
375
            shutil.rmtree(tmpdir)
 
376
 
 
377
    def iter_versions(self):
 
378
        """Iterate over all upstream versions.
 
379
 
 
380
        :return: Iterator over (tag_name, version, revid) tuples
 
381
        """
 
382
        for tag_name, tag_revid in self.branch.tags.get_tag_dict().iteritems():
 
383
            if not is_upstream_tag(tag_name):
 
384
                continue
 
385
            yield (tag_name, upstream_tag_version(tag_name), tag_revid)
 
386
 
 
387
 
 
388
def is_upstream_tag(tag):
 
389
    """Return true if tag is an upstream tag.
 
390
 
 
391
    :param tag: The string name of the tag.
 
392
    :return: True if the tag name is one generated by upstream tag operations.
 
393
    """
 
394
    return tag.startswith('upstream-') or tag.startswith('upstream/')
 
395
 
 
396
 
 
397
def upstream_tag_version(tag):
 
398
    """Return the upstream version portion of an upstream tag name.
 
399
 
 
400
    :param tag: The string name of the tag.
 
401
    :return: The version portion of the tag.
 
402
    """
 
403
    assert is_upstream_tag(tag), "Not an upstream tag: %s" % tag
 
404
    if tag.startswith('upstream/'):
 
405
        tag = tag[len('upstream/'):]
 
406
    elif tag.startswith('upstream-'):
 
407
        tag = tag[len('upstream-'):]
 
408
        if tag.startswith('debian-'):
 
409
            tag = tag[len('debian-'):]
 
410
        elif tag.startswith('ubuntu-'):
 
411
            tag = tag[len('ubuntu-'):]
 
412
    return tag