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 not 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 |
---|