File: testarchive.py

package info (click to toggle)
autopkgtest 5.53
  • links: PTS, VCS
  • area: main
  • in suites: forky, sid
  • size: 1,600 kB
  • sloc: python: 15,484; sh: 2,317; makefile: 116; perl: 19
file content (310 lines) | stat: -rw-r--r-- 11,025 bytes parent folder | download
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
"""Provide a fake package archive for testing"""

# (C) 2012-2015 Martin Pitt <martin.pitt@ubuntu.com>
#
# 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.

import tempfile
import shutil
import os
import subprocess
import atexit
import textwrap


class Archive:
    def __init__(
        self,
        *,
        path=None,
        pooldir="pool",
        suite=None,
        codename=None,
        component=None,
        notautomatic=False,
        butautomaticupgrades=False,
        codename_symlink=False,
    ):
        """Construct a local package test archive.

        By default, Packages.gz is created in the root of the target directory,
        so that the apt source will just use '/' for the suite and no
        components. If suite, codename and component are given, Packages.gz
        will be placed into 'dists/suite/component/binary-<arch>/', and the
        Suite and Codename fields of the Release file of this archive will be
        set to the specified values.

        If the notautomatic argument is true, "NotAutomatic: yes" will be set
        in the Release file. In a similar fashion, the butautomaticupgrades
        argument controls ButAutomaticUpgrades.

        If codename_symlink is true, the archive will be accessible by using
        the specified codename in the APT sources. This is accomplished by
        creating a dists/codename -> dists/suite symlink. This is useful to
        create archives with a Debian-like layout, where "unstable" is also
        available via its codename "sid".

        The archive is initially empty. You can create new packages with
        create_deb(). self.path contains the path of the archive, and
        self.apt_source provides an apt source "deb" line.

        If path is None (default), it is kept in a temporary directory which
        gets removed when the Archive object gets deleted. Otherwise the given
        path is used, which is useful for creating multiple suites/components;
        then you should use a different pooldir.
        """
        if path:
            self.path = path
        else:
            self.path = tempfile.mkdtemp(prefix="testarchive.")
            atexit.register(shutil.rmtree, self.path)
        self.pooldir = pooldir

        arch = subprocess.check_output(
            ["dpkg", "--print-architecture"], universal_newlines=True
        ).strip()
        self.suite = suite
        self.codename = codename
        self.component = component
        self.notautomatic = notautomatic
        self.butautomaticupgrades = butautomaticupgrades
        if suite or codename or component:
            assert suite and codename and component, (
                "must specify all of none of: suite, codename, component"
            )
            self.index_dir = os.path.join("dists", suite, component, "binary-" + arch)
            os.makedirs(os.path.join(self.path, self.index_dir))
            self.apt_source = "deb [trusted=yes arch=%s] file://%s %s %s" % (
                arch,
                self.path,
                suite,
                component,
            )

            if codename_symlink and codename != suite:
                os.symlink(suite, os.path.join(self.path, "dists", codename))
        else:
            self.apt_source = "deb [trusted=yes arch=%s] file://%s /" % (
                arch,
                self.path,
            )
            self.index_dir = ""

    def create_deb(
        self,
        name,
        version="1",
        architecture="all",
        dependencies={},
        description="test package",
        extra_tags={},
        files={},
        component="main",
        srcpkg=None,
        update_index=True,
    ):
        """Build a deb package and add it to the archive.

        The only mandatory argument is the package name. You can additionally
        specify the package version (default '1'), architecture (default
        'all'), a dictionary with dependencies (empty by default; for example
        {'Depends': 'foo, bar', 'Conflicts: baz'}, a short description
        (default: 'test package'), and arbitrary extra tags.

        By default the package is empty. It can get files by specifying a
        path -> contents dictionary in 'files'. Paths must be relative.
        Example: files={'etc/foo.conf': 'enable=true'}

        The newly created deb automatically gets added to the "Packages" index,
        unless update_index is False.

        Return the path to the newly created deb package, in case you only need
        the deb itself, not the archive.
        """
        d = tempfile.mkdtemp()
        os.mkdir(os.path.join(d, "DEBIAN"))
        with open(os.path.join(d, "DEBIAN", "control"), "w") as f:
            f.write(
                """Package: %s
Maintainer: Test User <test@example.com>
Version: %s
Priority: optional
Section: devel
Architecture: %s
"""
                % (name, version, architecture)
            )

            if srcpkg:
                f.write("Source: %s\n" % srcpkg)

            for k, v in dependencies.items():
                f.write("%s: %s\n" % (k, v))

            f.write(
                """Description: %s
 Test dummy package.
"""
                % description
            )

            for k, v in extra_tags.items():
                f.write("%s: %s\n" % (k, v))

        for path, contents in files.items():
            if isinstance(contents, bytes):
                mode = "wb"
            else:
                mode = "w"
            pathdir = os.path.join(d, os.path.dirname(path))
            if not os.path.isdir(pathdir):
                os.makedirs(pathdir)
            with open(os.path.join(d, path), mode) as f:
                f.write(contents)

        if srcpkg is None:
            srcpkg = name
        if srcpkg.startswith("lib"):
            prefix = srcpkg[:4]
        else:
            prefix = srcpkg[0]
        dir = os.path.join(self.path, self.pooldir, component, prefix, srcpkg)
        if not os.path.isdir(dir):
            os.makedirs(dir)

        debpath = os.path.join(dir, "%s_%s_%s.deb" % (name, version, architecture))
        subprocess.check_call(
            ["dpkg-deb", "-Zgzip", "-b", d, debpath],
            stdout=subprocess.PIPE,
            stderr=subprocess.PIPE,
        )

        shutil.rmtree(d)
        assert os.path.exists(debpath)

        if update_index:
            self.update_index()

        return debpath

    def add_sources(self, name, binaries, version="1", component="main"):
        """Add source package entry to the Sources index"""

        if type(binaries) is not dict:
            binaries = {k: "any" for k in binaries}

        if name.startswith("lib"):
            prefix = name[:4]
        else:
            prefix = name[0]
        poolsubdir = os.path.join(self.pooldir, self.component, prefix, name)

        # create a dsc
        dir = os.path.join(self.path, self.pooldir, component, prefix, name)
        if not os.path.isdir(dir):
            os.makedirs(dir)
        dscpath = os.path.join(dir, "%s_%s.dsc" % (name, version))
        with open(dscpath, mode="w"):
            # For now we only care about the file being present,
            # so let's create it empty.
            pass

        if self.index_dir:
            srcindex_dir = os.path.join(
                self.path, os.path.dirname(self.index_dir), "source"
            )
        else:
            srcindex_dir = self.index_dir
        if not os.path.isdir(srcindex_dir):
            os.makedirs(srcindex_dir)

        with open(os.path.join(srcindex_dir, "Sources"), "a") as f:
            f.write(
                textwrap.dedent(
                    f"""\
                    Package: {name}
                    Binary: {", ".join(binaries.keys())}
                    Version: {version}
                    Priority: optional
                    Architecture: any all
                    Format: 1.0
                    Directory: {poolsubdir}
                    Files:
                     d41d8cd98f00b204e9800998ecf8427e 0 {name}_{version}.dsc
                    Package-List:
                    """
                )
            )
            for b, a in binaries.items():
                f.write(" %s deb admin optional arch=%s\n" % (b, a))
            f.write("Standards-Version: 1.0\n\n")

    def update_index(self):
        """Update the Packages index and Release file.

        This usually gets done automatically by create_deb(), but needs to be
        done if you manually copy debs into the archive or call create_deb with
        update_index==False.
        """
        old_cwd = os.getcwd()
        try:
            os.chdir(self.path)
            with open(os.path.join(self.index_dir, "Packages"), "wb") as f:
                subprocess.check_call(
                    ["apt-ftparchive", "packages", self.pooldir], stdout=f
                )
            if self.suite:
                rp = os.path.join(self.path, "dists", self.suite, "Release")
                try:
                    os.unlink(rp)
                except OSError:
                    pass
                release_cmd = [
                    "apt-ftparchive",
                    "release",
                    "-o",
                    "APT::FTPArchive::Release::Suite=" + self.suite,
                    "-o",
                    "APT::FTPArchive::Release::Codename=" + self.codename,
                ]
                if self.notautomatic:
                    release_cmd += ["-o", "APT::FTPArchive::Release::NotAutomatic=yes"]
                if self.butautomaticupgrades:
                    release_cmd += [
                        "-o",
                        "APT::FTPArchive::Release::ButAutomaticUpgrades=yes",
                    ]
                release_cmd += [os.path.dirname(rp)]
                release = subprocess.check_output(release_cmd)
                with open(rp, "wb") as f:
                    f.write(release)
        finally:
            os.chdir(old_cwd)
        subprocess.check_call(["chmod", "-R", "a+rX", "--", self.path])


if __name__ == "__main__":
    r = Archive(suite="testy", component="main")
    r.create_deb("vanilla")
    r.create_deb("libvanilla0", srcpkg="vanilla")
    r.create_deb("chocolate", dependencies={"Depends": "vanilla"})
    print(r.apt_source)
    r.add_sources("vanilla", ["vanilla", "libvanilla0"])

    p = Archive(
        path=r.path,
        pooldir="pool-proposed",
        suite="testy-proposed",
        codename="testy",
        component="main",
    )
    p.create_deb("vanilla", "2")
    r.create_deb("libvanilla0", "2", srcpkg="vanilla")
    p.create_deb("chocolate", "2", dependencies={"Depends": "vanilla (>= 2)"})
    print(p.apt_source)
    p.add_sources("vanilla", ["vanilla", "libvanilla0"], "2")

    subprocess.call(["bash", "-i"], cwd=r.path)