Fixed conflicts from version vs dev
[invirt/packages/invirt-dev.git] / python / invirt / builder.py
1 """Invirt build utilities.
2
3 This module contains utility functions used by both the invirtibuilder
4 and the remctl submission scripts that insert items into its queue.
5 """
6
7
8 import os
9 import subprocess
10
11 from debian_bundle import changelog
12
13 import invirt.common as c
14 from invirt.config import structs as config
15
16
17 _QUEUE_DIR = '/var/lib/invirt-dev/queue'
18 _REPO_DIR = '/srv/git'
19 _LOG_DIR = '/var/log/invirt/builds'
20 _HOOKS_DIR = '/usr/share/invirt-dev/build-hooks'
21
22 _DEFAULT_DISTRIBUTION = 'hardy'
23
24
25 class InvalidBuild(ValueError):
26     pass
27
28 _DISTRO_TO_SUFFIX = {
29     'etch': '~debian4.0',
30     'lenny': '~debian5.0',
31     'squeeze': '~debian6.0',
32
33     'hardy': '~ubuntu8.04',
34     'lucid': '~ubuntu10.04',
35     'maverick': '~ubuntu10.10',
36     'natty': '~ubuntu11.04',
37     'oneiric': '~ubuntu11.10',
38     'precise': '~ubuntu12.04',
39     }
40
41 def distroToSuffix(distro):
42     return _DISTRO_TO_SUFFIX.get(distro, '~'+distro)
43
44 def getRepo(package):
45     """Return the path to the git repo for a given package."""
46     return os.path.join(_REPO_DIR, 'invirt/packages', '%s.git' % package)
47
48 def ensureValidPackage(package):
49     """Perform some basic sanity checks that the requested repo is in a
50     subdirectory of _REPO_DIR/invirt/packages.  This prevents weirdness
51     such as submitting a package like '../prod/...git'.  Also ensures that
52     the repo exists."""
53     # TODO: this might be easier just to regex
54     repo = os.path.abspath(getRepo(package))
55     parent_dir = os.path.dirname(repo)
56     prefix = os.path.join(_REPO_DIR, 'invirt/packages')
57     if not parent_dir.startswith(prefix):
58         raise InvalidBuild('Invalid package name %s' % package)
59     elif not os.path.exists(repo):
60         raise InvalidBuild('Nonexisting package %s' % package)
61
62 def canonicalize_commit(package, commit, shorten=False):
63     if shorten:
64         flags = ['--short']
65     else:
66         flags = []
67     return c.captureOutput(['git', 'rev-parse'] + flags + [commit],
68                            cwd=getRepo(package)).strip()
69
70 def pocketToGit(pocket):
71     """Map a pocket in the configuration to a git branch."""
72     return getattr(getattr(config.build.pockets, pocket), 'git', pocket)
73
74
75 def pocketToApt(pocket):
76     """Map a pocket in the configuration to an apt repo pocket."""
77     return getattr(getattr(config.build.pockets, pocket), 'apt', pocket)
78
79 def pocketToDistro(pocket):
80     """Map a pocket in the configuration to the distro we build for."""
81     return getattr(getattr(config.build.pockets, pocket), 'distro', _DEFAULT_DISTRIBUTION)
82
83 def getGitFile(package, ref, path):
84     """Return the contents of a path from a git ref in a package."""
85     return c.captureOutput(['git', 'cat-file', 'blob', '%s:%s' % (ref, path)],
86                            cwd=getRepo(package))
87
88
89 def getChangelog(package, ref):
90     """Get a changelog object for a given ref in a given package.
91
92     This returns a debian_bundle.changelog.Changelog object for a
93     given ref of a given package.
94     """
95     return changelog.Changelog(getGitFile(package, ref, 'debian/changelog'))
96
97 def runHook(hook, args=[], stdin_str=None):
98     """Run a named hook."""
99     hook = os.path.join(_HOOKS_DIR, hook)
100     try:
101         c.captureOutput([hook] + args, stdin_str=stdin_str)
102     except OSError:
103         pass
104
105 def getVersion(package, ref):
106     """Get the version of a given package at a particular ref."""
107     return getChangelog(package, ref).get_version()
108
109
110 def validateBuild(pocket, package, commit):
111     """Given the parameters of a new build, validate that build.
112
113     The checks this function performs vary based on whether or not the
114     pocket is configured with allow_backtracking.
115
116     A build of a pocket without allow_backtracking set must be a
117     fast-forward of the previous revision, and the most recent version
118     in the changelog most be strictly greater than the version
119     currently in the repository.
120
121     In all cases, this revision of the package can only have the same
122     version number as any other revision currently in the apt
123     repository if they have the same commit ID.
124
125     If it's unspecified, it is assumed that pocket do not
126     allow_backtracking.
127
128     If this build request fails validation, this function will raise a
129     InvalidBuild exception, with information about why the validation
130     failed.
131
132     If this build request can be satisfied by copying the package from
133     another pocket, then this function returns that pocket. Otherwise,
134     it returns True.
135     """
136     ensureValidPackage(package)
137     package_repo = getRepo(package)
138     new_version = getVersion(package, commit)
139     new_distro = pocketToDistro(pocket)
140
141     ret = True
142
143     for p in config.build.pockets:
144         if p == pocket:
145             continue
146
147         b = pocketToGit(p)
148         try:
149             current_commit = c.captureOutput(['git', 'rev-parse', b],
150                                              cwd=package_repo).strip()
151         except subprocess.CalledProcessError:
152             # Guess we haven't created this pocket yet
153             continue
154
155         current_version = getVersion(package, b)
156         current_distro = pocketToDistro(p)
157
158         # NB: Neither current_version nor new_version will have the
159         # distro-specific prefix.
160
161         if current_version == new_version and current_distro == new_distro:
162             if current_commit == commit:
163                 ret = p
164             else:
165                 raise InvalidBuild('Version %s of %s already available is in '
166                                    'pocket %s from commit %s' %
167                                    (new_version, package, p, current_commit))
168
169     if not config.build.pockets[pocket].get('allow_backtracking', False):
170         branch = pocketToGit(pocket)
171         current_version = getVersion(package, branch)
172         if new_version <= current_version:
173             raise InvalidBuild('New version %s of %s is not newer than '
174                                'version %s currently in pocket %s' %
175                                (new_version, package, current_version, pocket))
176
177         # Almost by definition, A is a fast-forward of B if B..A is
178         # empty
179         if c.captureOutput(['git', 'rev-list', '%s..%s' % (commit, branch)],
180                            cwd=package_repo):
181             raise InvalidBuild('New commit %s of %s is not a fast-forward of'
182                                'commit currently in pocket %s' %
183                                (commit, package, pocket))
184
185     return ret