Remove stray 'not' in builder.py
[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
23 class InvalidBuild(ValueError):
24     pass
25
26
27 def getRepo(package):
28     """Return the path to the git repo for a given package."""
29     return os.path.join(_REPO_DIR, 'invirt/packages', '%s.git' % package)
30
31 def ensureValidPackage(package):
32     """Perform some basic sanity checks that the requested repo is in a
33     subdirectory of _REPO_DIR/invirt/packages.  This prevents weirdness
34     such as submitting a package like '../prod/...git'.  Also ensures that
35     the repo exists."""
36     # TODO: this might be easier just to regex
37     repo = os.path.abspath(getRepo(package))
38     parent_dir = os.path.dirname(repo)
39     prefix = os.path.join(_REPO_DIR, 'invirt/packages')
40     if not parent_dir.startswith(prefix):
41         raise InvalidBuild('Invalid package name %s' % package)
42     elif not os.path.exists(repo):
43         raise InvalidBuild('Nonexisting package %s' % package)
44
45 def canonicalize_commit(package, commit, shorten=False):
46     if shorten:
47         flags = ['--short']
48     else:
49         flags = []
50     return c.captureOutput(['git', 'rev-parse'] + flags + [commit],
51                            cwd=getRepo(package)).strip()
52
53 def pocketToGit(pocket):
54     """Map a pocket in the configuration to a git branch."""
55     return getattr(getattr(config.build.pockets, pocket), 'git', pocket)
56
57
58 def pocketToApt(pocket):
59     """Map a pocket in the configuration to an apt repo pocket."""
60     return getattr(getattr(config.build.pockets, pocket), 'apt', pocket)
61
62
63 def getGitFile(package, ref, path):
64     """Return the contents of a path from a git ref in a package."""
65     return c.captureOutput(['git', 'cat-file', 'blob', '%s:%s' % (ref, path)],
66                            cwd=getRepo(package))
67
68
69 def getChangelog(package, ref):
70     """Get a changelog object for a given ref in a given package.
71
72     This returns a debian_bundle.changelog.Changelog object for a
73     given ref of a given package.
74     """
75     return changelog.Changelog(getGitFile(package, ref, 'debian/changelog'))
76
77 def runHook(hook, args=[], stdin_str=None):
78     """Run a named hook."""
79     hook = os.path.join(_HOOKS_DIR, hook)
80     try:
81         c.captureOutput([hook] + args, stdin_str=stdin_str)
82     except OSError:
83         pass
84
85 def getVersion(package, ref):
86     """Get the version of a given package at a particular ref."""
87     return getChangelog(package, ref).get_version()
88
89
90 def validateBuild(pocket, package, commit):
91     """Given the parameters of a new build, validate that build.
92
93     The checks this function performs vary based on whether or not the
94     pocket is configured with allow_backtracking.
95
96     A build of a pocket without allow_backtracking set must be a
97     fast-forward of the previous revision, and the most recent version
98     in the changelog most be strictly greater than the version
99     currently in the repository.
100
101     In all cases, this revision of the package can only have the same
102     version number as any other revision currently in the apt
103     repository if they have the same commit ID.
104
105     If it's unspecified, it is assumed that pocket do not
106     allow_backtracking.
107
108     If this build request fails validation, this function will raise a
109     InvalidBuild exception, with information about why the validation
110     failed.
111
112     If this build request can be satisfied by copying the package from
113     another pocket, then this function returns that pocket. Otherwise,
114     it returns True.
115     """
116     ensureValidPackage(package)
117     package_repo = getRepo(package)
118     new_version = getVersion(package, commit)
119
120     ret = True
121
122     for p in config.build.pockets:
123         if p == pocket:
124             continue
125
126         b = pocketToGit(p)
127         try:
128             current_commit = c.captureOutput(['git', 'rev-parse', b],
129                                              cwd=package_repo).strip()
130         except subprocess.CalledProcessError:
131             # Guess we haven't created this pocket yet
132             continue
133
134         current_version = getVersion(package, b)
135
136         if current_version == new_version:
137             if current_commit == commit:
138                 ret = p
139             else:
140                 raise InvalidBuild('Version %s of %s already available is in '
141                                    'pocket %s from commit %s' %
142                                    (new_version, package, p, current_commit))
143
144     if not config.build.pockets[pocket].get('allow_backtracking', False):
145         branch = pocketToGit(pocket)
146         current_version = getVersion(package, branch)
147         if new_version <= current_version:
148             raise InvalidBuild('New version %s of %s is not newer than '
149                                'version %s currently in pocket %s' %
150                                (new_version, package, current_version, pocket))
151
152         # Almost by definition, A is a fast-forward of B if B..A is
153         # empty
154         if c.captureOutput(['git', 'rev-list', '%s..%s' % (commit, branch)],
155                            cwd=package_repo):
156             raise InvalidBuild('New commit %s of %s is not a fast-forward of'
157                                'commit currently in pocket %s' %
158                                (commit, package, pocket))
159
160     return ret