Rietveld Code Review Tool
Help | Bug tracker | Discussion group | Source code

Side by Side Diff: abb-build/ensure_dependencies.py

Issue 29552627: Issue 4105 - [build] Make it possible to build arbitrary ABB revisions (Closed)
Patch Set: Created Sept. 22, 2017, 11:55 a.m.
Left:
Right:
Use n/p to move between diff chunks; N/P to move between comments.
Jump to:
View unified diff | Download patch
OLDNEW
(Empty)
1 #!/usr/bin/env python
2
3 # This Source Code Form is subject to the terms of the Mozilla Public
4 # License, v. 2.0. If a copy of the MPL was not distributed with this
5 # file, You can obtain one at http://mozilla.org/MPL/2.0/.
6
7 import sys
8 import os
9 import posixpath
10 import re
11 import io
12 import errno
13 import logging
14 import subprocess
15 import urlparse
16 import argparse
17
18 from collections import OrderedDict
19 from ConfigParser import RawConfigParser
20
21 USAGE = '''
22 A dependencies file should look like this:
23
24 # VCS-specific root URLs for the repositories
25 _root = hg:https://hg.adblockplus.org/ git:https://github.com/adblockplus/
26 # File to update this script from (optional)
27 _self = buildtools/ensure_dependencies.py
28 # Clone elemhidehelper repository into extensions/elemhidehelper directory at
29 # tag "1.2".
30 extensions/elemhidehelper = elemhidehelper 1.2
31 # Clone buildtools repository into buildtools directory at VCS-specific
32 # revision IDs.
33 buildtools = buildtools hg:016d16f7137b git:f3f8692f82e5
34 # Clone the adblockplus repository into adblockplus directory, overwriting the
35 # usual source URL for Git repository and specifying VCS specific revision IDs .
36 adblockplus = adblockplus hg:893426c6a6ab git:git@github.com:user/adblockplus. git@b2ffd52b
37 # Clone the adblockpluschrome repository into the adblockpluschrome directory,
38 # from a specific Git repository, specifying the revision ID.
39 adblockpluschrome = git:git@github.com:user/adblockpluschrome.git@1fad3a7
40 '''
41
42 SKIP_DEPENDENCY_UPDATES = os.environ.get(
43 'SKIP_DEPENDENCY_UPDATES', ''
44 ).lower() not in ('', '0', 'false')
45
46
47 class Mercurial():
48 def istype(self, repodir):
49 return os.path.exists(os.path.join(repodir, '.hg'))
50
51 def clone(self, source, target):
52 if not source.endswith('/'):
53 source += '/'
54 subprocess.check_call(['hg', 'clone', '--quiet', '--noupdate', source, t arget])
55
56 def get_revision_id(self, repo, rev=None):
57 command = ['hg', 'id', '--repository', repo, '--id']
58 if rev:
59 command.extend(['--rev', rev])
60
61 # Ignore stderr output and return code here: if revision lookup failed w e
62 # should simply return an empty string.
63 result = subprocess.Popen(command, stdout=subprocess.PIPE, stderr=subpro cess.PIPE).communicate()[0]
64 return result.strip()
65
66 def pull(self, repo):
67 subprocess.check_call(['hg', 'pull', '--repository', repo, '--quiet'])
68
69 def update(self, repo, rev, revname):
70 subprocess.check_call(['hg', 'update', '--repository', repo, '--quiet', '--check', '--rev', rev])
71
72 def ignore(self, target, repo):
73 config_path = os.path.join(repo, '.hg', 'hgrc')
74 ignore_file = os.path.join('.hg', 'dependencies')
75 ignore_path = os.path.join(repo, ignore_file)
76
77 config = RawConfigParser()
78 config.read(config_path)
79
80 if not config.has_section('ui'):
81 config.add_section('ui')
82
83 config.set('ui', 'ignore.dependencies', ignore_file)
84 with open(config_path, 'w') as stream:
85 config.write(stream)
86
87 module = os.path.relpath(target, repo)
88 _ensure_line_exists(ignore_path, module)
89
90 def postprocess_url(self, url):
91 return url
92
93
94 class Git():
95 def istype(self, repodir):
96 return os.path.exists(os.path.join(repodir, '.git'))
97
98 def clone(self, source, target):
99 source = source.rstrip('/')
100 if not source.endswith('.git'):
101 source += '.git'
102 subprocess.check_call(['git', 'clone', '--quiet', source, target])
103
104 def get_revision_id(self, repo, rev='HEAD'):
105 command = ['git', 'rev-parse', '--revs-only', rev + '^{commit}']
106 return subprocess.check_output(command, cwd=repo).strip()
107
108 def pull(self, repo):
109 # Fetch tracked branches, new tags and the list of available remote bran ches
110 subprocess.check_call(['git', 'fetch', '--quiet', '--all', '--tags'], cw d=repo)
111 # Next we need to ensure all remote branches are tracked
112 newly_tracked = False
113 remotes = subprocess.check_output(['git', 'branch', '--remotes'], cwd=re po)
114 for match in re.finditer(r'^\s*(origin/(\S+))$', remotes, re.M):
115 remote, local = match.groups()
116 with open(os.devnull, 'wb') as devnull:
117 if subprocess.call(['git', 'branch', '--track', local, remote],
118 cwd=repo, stdout=devnull, stderr=devnull) == 0:
119 newly_tracked = True
120 # Finally fetch any newly tracked remote branches
121 if newly_tracked:
122 subprocess.check_call(['git', 'fetch', '--quiet', 'origin'], cwd=rep o)
123
124 def update(self, repo, rev, revname):
125 subprocess.check_call(['git', 'checkout', '--quiet', revname], cwd=repo)
126
127 def ignore(self, target, repo):
128 module = os.path.sep + os.path.relpath(target, repo)
129 exclude_file = os.path.join(repo, '.git', 'info', 'exclude')
130 _ensure_line_exists(exclude_file, module)
131
132 def postprocess_url(self, url):
133 # Handle alternative syntax of SSH URLS
134 if '@' in url and ':' in url and not urlparse.urlsplit(url).scheme:
135 return 'ssh://' + url.replace(':', '/', 1)
136 return url
137
138 repo_types = OrderedDict((
139 ('hg', Mercurial()),
140 ('git', Git()),
141 ))
142
143 # [vcs:]value
144 item_regexp = re.compile(
145 '^(?:(' + '|'.join(map(re.escape, repo_types.keys())) + '):)?'
146 '(.+)$'
147 )
148
149 # [url@]rev
150 source_regexp = re.compile(
151 '^(?:(.*)@)?'
152 '(.+)$'
153 )
154
155
156 def merge_seqs(seq1, seq2):
157 """Return a list of any truthy values from the suplied sequences
158
159 (None, 2), (1,) => [1, 2]
160 None, (1, 2) => [1, 2]
161 (1, 2), (3, 4) => [3, 4]
162 """
163 return map(lambda item1, item2: item2 or item1, seq1 or (), seq2 or ())
164
165
166 def parse_spec(path, line):
167 if '=' not in line:
168 logging.warning('Invalid line in file %s: %s' % (path, line))
169 return None, None
170
171 key, value = line.split('=', 1)
172 key = key.strip()
173 items = value.split()
174 if not len(items):
175 logging.warning('No value specified for key %s in file %s' % (key, path) )
176 return key, None
177
178 result = OrderedDict()
179 is_dependency_field = not key.startswith('_')
180
181 for i, item in enumerate(items):
182 try:
183 vcs, value = re.search(item_regexp, item).groups()
184 vcs = vcs or '*'
185 if is_dependency_field:
186 if i == 0 and vcs == '*':
187 # In order to be backwards compatible we have to assume that the first
188 # source contains only a URL/path for the repo if it does no t contain
189 # the VCS part
190 url_rev = (value, None)
191 else:
192 url_rev = re.search(source_regexp, value).groups()
193 result[vcs] = merge_seqs(result.get(vcs), url_rev)
194 else:
195 if vcs in result:
196 logging.warning('Ignoring duplicate value for type %r '
197 '(key %r in file %r)' % (vcs, key, path))
198 result[vcs] = value
199 except AttributeError:
200 logging.warning('Ignoring invalid item %r for type %r '
201 '(key %r in file %r)' % (item, vcs, key, path))
202 continue
203 return key, result
204
205
206 def read_deps(repodir):
207 result = {}
208 deps_path = os.path.join(repodir, 'dependencies')
209 try:
210 with io.open(deps_path, 'rt', encoding='utf-8') as handle:
211 for line in handle:
212 # Remove comments and whitespace
213 line = re.sub(r'#.*', '', line).strip()
214 if not line:
215 continue
216
217 key, spec = parse_spec(deps_path, line)
218 if spec:
219 result[key] = spec
220 return result
221 except IOError as e:
222 if e.errno != errno.ENOENT:
223 raise
224 return None
225
226
227 def safe_join(path, subpath):
228 # This has been inspired by Flask's safe_join() function
229 forbidden = {os.sep, os.altsep} - {posixpath.sep, None}
230 if any(sep in subpath for sep in forbidden):
231 raise Exception('Illegal directory separator in dependency path %s' % su bpath)
232
233 normpath = posixpath.normpath(subpath)
234 if posixpath.isabs(normpath):
235 raise Exception('Dependency path %s cannot be absolute' % subpath)
236 if normpath == posixpath.pardir or normpath.startswith(posixpath.pardir + po sixpath.sep):
237 raise Exception('Dependency path %s has to be inside the repository' % s ubpath)
238 return os.path.join(path, *normpath.split(posixpath.sep))
239
240
241 def get_repo_type(repo):
242 for name, repotype in repo_types.iteritems():
243 if repotype.istype(repo):
244 return name
245 return 'hg'
246
247
248 def ensure_repo(parentrepo, parenttype, target, type, root, sourcename):
249 if os.path.exists(target):
250 return
251
252 if SKIP_DEPENDENCY_UPDATES:
253 logging.warning('SKIP_DEPENDENCY_UPDATES environment variable set, '
254 '%s not cloned', target)
255 return
256
257 postprocess_url = repo_types[type].postprocess_url
258 root = postprocess_url(root)
259 sourcename = postprocess_url(sourcename)
260
261 if os.path.exists(root):
262 url = os.path.join(root, sourcename)
263 else:
264 url = urlparse.urljoin(root, sourcename)
265
266 logging.info('Cloning repository %s into %s' % (url, target))
267 repo_types[type].clone(url, target)
268 repo_types[parenttype].ignore(target, parentrepo)
269
270
271 def update_repo(target, type, revision):
272 resolved_revision = repo_types[type].get_revision_id(target, revision)
273 current_revision = repo_types[type].get_revision_id(target)
274
275 if resolved_revision != current_revision:
276 if SKIP_DEPENDENCY_UPDATES:
277 logging.warning('SKIP_DEPENDENCY_UPDATES environment variable set, '
278 '%s not checked out to %s', target, revision)
279 return
280
281 if not resolved_revision:
282 logging.info('Revision %s is unknown, downloading remote changes' % revision)
283 repo_types[type].pull(target)
284 resolved_revision = repo_types[type].get_revision_id(target, revisio n)
285 if not resolved_revision:
286 raise Exception('Failed to resolve revision %s' % revision)
287
288 logging.info('Updating repository %s to revision %s' % (target, resolved _revision))
289 repo_types[type].update(target, resolved_revision, revision)
290
291
292 def resolve_deps(repodir, level=0, self_update=True, overrideroots=None, skipdep endencies=set()):
293 config = read_deps(repodir)
294 if config is None:
295 if level == 0:
296 logging.warning('No dependencies file in directory %s, nothing to do ...\n%s' % (repodir, USAGE))
297 return
298 if level >= 10:
299 logging.warning('Too much subrepository nesting, ignoring %s' % repo)
300 return
301
302 if overrideroots is not None:
303 config['_root'] = overrideroots
304
305 for dir, sources in config.iteritems():
306 if (dir.startswith('_') or
307 skipdependencies.intersection([s[0] for s in sources if s[0]])):
308 continue
309
310 target = safe_join(repodir, dir)
311 parenttype = get_repo_type(repodir)
312 _root = config.get('_root', {})
313
314 for key in sources.keys() + _root.keys():
315 if key == parenttype or key is None and vcs != '*':
316 vcs = key
317 source, rev = merge_seqs(sources.get('*'), sources.get(vcs))
318
319 if not (vcs and source and rev):
320 logging.warning('No valid source / revision found to create %s' % ta rget)
321 continue
322
323 ensure_repo(repodir, parenttype, target, vcs, _root.get(vcs, ''), source )
324 update_repo(target, vcs, rev)
325 resolve_deps(target, level + 1, self_update=False,
326 overrideroots=overrideroots, skipdependencies=skipdependenc ies)
327
328 if self_update and '_self' in config and '*' in config['_self']:
329 source = safe_join(repodir, config['_self']['*'])
330 try:
331 with io.open(source, 'rb') as handle:
332 sourcedata = handle.read()
333 except IOError as e:
334 if e.errno != errno.ENOENT:
335 raise
336 logging.warning("File %s doesn't exist, skipping self-update" % sour ce)
337 return
338
339 target = __file__
340 with io.open(target, 'rb') as handle:
341 targetdata = handle.read()
342
343 if sourcedata != targetdata:
344 logging.info("Updating %s from %s, don't forget to commit" % (target , source))
345 with io.open(target, 'wb') as handle:
346 handle.write(sourcedata)
347 if __name__ == '__main__':
348 logging.info('Restarting %s' % target)
349 os.execv(sys.executable, [sys.executable, target] + sys.argv[1:] )
350 else:
351 logging.warning('Cannot restart %s automatically, please rerun' % target)
352
353
354 def _ensure_line_exists(path, pattern):
355 with open(path, 'a+') as f:
356 f.seek(0, os.SEEK_SET)
357 file_content = [l.strip() for l in f.readlines()]
358 if not pattern in file_content:
359 file_content.append(pattern)
360 f.seek(0, os.SEEK_SET)
361 f.truncate()
362 for l in file_content:
363 print >>f, l
364
365 if __name__ == '__main__':
366 logging.basicConfig(format='%(levelname)s: %(message)s', level=logging.INFO)
367
368 parser = argparse.ArgumentParser(description='Verify dependencies for a set of repositories, by default the repository of this script.')
369 parser.add_argument('repos', metavar='repository', type=str, nargs='*', help ='Repository path')
370 parser.add_argument('-q', '--quiet', action='store_true', help='Suppress inf ormational output')
371 args = parser.parse_args()
372
373 if args.quiet:
374 logging.disable(logging.INFO)
375
376 repos = args.repos
377 if not len(repos):
378 repos = [os.path.dirname(__file__)]
379 for repo in repos:
380 resolve_deps(repo)
OLDNEW
« abb-build/dependencies ('K') | « abb-build/dependencies ('k') | abb-build/mozconfig-arm » ('j') | no next file with comments »

Powered by Google App Engine
This is Rietveld