Update To 11.40.268.0
[platform/framework/web/crosswalk.git] / src / third_party / chromite / cbuildbot / repository.py
1 # Copyright (c) 2012 The Chromium OS Authors. All rights reserved.
2 # Use of this source code is governed by a BSD-style license that can be
3 # found in the LICENSE file.
4
5 """Repository module to handle different types of repositories."""
6
7 from __future__ import print_function
8
9 import constants
10 import logging
11 import os
12 import re
13 import shutil
14
15 from chromite.lib import cros_build_lib
16 from chromite.lib import git
17 from chromite.lib import osutils
18 from chromite.lib import rewrite_git_alternates
19 from chromite.lib import retry_util
20
21 # File that marks a buildroot as being used by a trybot
22 _TRYBOT_MARKER = '.trybot'
23
24
25 class SrcCheckOutException(Exception):
26   """Exception gets thrown for failure to sync sources"""
27   pass
28
29
30 def IsARepoRoot(directory):
31   """Returns True if directory is the root of a repo checkout."""
32   return os.path.exists(os.path.join(directory, '.repo'))
33
34
35 def IsInternalRepoCheckout(root):
36   """Returns whether root houses an internal 'repo' checkout."""
37   manifest_dir = os.path.join(root, '.repo', 'manifests')
38   manifest_url = git.RunGit(
39       manifest_dir, ['config', 'remote.origin.url']).output.strip()
40   return (os.path.splitext(os.path.basename(manifest_url))[0]
41           == os.path.splitext(os.path.basename(constants.MANIFEST_INT_URL))[0])
42
43
44 def CloneGitRepo(working_dir, repo_url, reference=None, bare=False,
45                  mirror=False, depth=None):
46   """Clone given git repo
47
48   Args:
49     working_dir: location where it should be cloned to
50     repo_url: git repo to clone
51     reference: If given, pathway to a git repository to access git objects
52       from.  Note that the reference must exist as long as the newly created
53       repo is to be usable.
54     bare: Clone a bare checkout.
55     mirror: Clone a mirror checkout.
56     depth: If given, do a shallow clone limiting the objects pulled to just
57       that # of revs of history.  This option is mutually exclusive to
58       reference.
59   """
60   osutils.SafeMakedirs(working_dir)
61   cmd = ['clone', repo_url, working_dir]
62   if reference:
63     if depth:
64       raise ValueError("reference and depth are mutually exclusive "
65                        "options; please pick one or the other.")
66     cmd += ['--reference', reference]
67   if bare:
68     cmd += ['--bare']
69   if mirror:
70     cmd += ['--mirror']
71   if depth:
72     cmd += ['--depth', str(int(depth))]
73   git.RunGit(working_dir, cmd)
74
75
76 def UpdateGitRepo(working_dir, repo_url, **kwargs):
77   """Update the given git repo, blowing away any local changes.
78
79   If the repo does not exist, clone it from scratch.
80
81   Args:
82     working_dir: location where it should be cloned to
83     repo_url: git repo to clone
84     **kwargs: See CloneGitRepo.
85   """
86   assert not kwargs.get('bare'), 'Bare checkouts are not supported'
87   if git.IsGitRepo(working_dir):
88     try:
89       git.CleanAndCheckoutUpstream(working_dir)
90     except cros_build_lib.RunCommandError:
91       cros_build_lib.Warning('Could not update %s', working_dir, exc_info=True)
92       shutil.rmtree(working_dir)
93       CloneGitRepo(working_dir, repo_url, **kwargs)
94   else:
95     CloneGitRepo(working_dir, repo_url, **kwargs)
96
97
98 def GetTrybotMarkerPath(buildroot):
99   """Get path to trybot marker file given the buildroot."""
100   return os.path.join(buildroot, _TRYBOT_MARKER)
101
102
103 def CreateTrybotMarker(buildroot):
104   """Create the file that identifies a buildroot as being used by a trybot."""
105   osutils.WriteFile(GetTrybotMarkerPath(buildroot), '')
106
107 def ClearBuildRoot(buildroot, preserve_paths=()):
108   """Remove and recreate the buildroot while preserving the trybot marker."""
109   trybot_root = os.path.exists(GetTrybotMarkerPath(buildroot))
110   if os.path.exists(buildroot):
111     cmd = ['find', buildroot, '-mindepth', '1', '-maxdepth', '1']
112
113     ignores = []
114     for path in preserve_paths:
115       if ignores:
116         ignores.append('-a')
117       ignores += ['!', '-name', path]
118     cmd.extend(ignores)
119
120     cmd += ['-exec', 'rm', '-rf', '{}', '+']
121     cros_build_lib.SudoRunCommand(cmd)
122   else:
123     os.makedirs(buildroot)
124   if trybot_root:
125     CreateTrybotMarker(buildroot)
126
127
128 class RepoRepository(object):
129   """A Class that encapsulates a repo repository.
130
131   Args:
132     manifest_repo_url: URL to fetch repo manifest from.
133     directory: local path where to checkout the repository.
134     branch: Branch to check out the manifest at.
135     referenced_repo: Repository to reference for git objects, if possible.
136     manifest: Which manifest.xml within the branch to use.  Effectively
137       default.xml if not given.
138     depth: Mutually exclusive option to referenced_repo; this limits the
139       checkout to a max commit history of the given integer.
140     repo_url: URL to fetch repo tool from.
141     repo_branch: Branch to check out the repo tool at.
142   """
143   # Use our own repo, in case android.kernel.org (the default location) is down.
144   _INIT_CMD = ['repo', 'init']
145
146   # If a repo hasn't been used in the last 5 runs, wipe it.
147   LRU_THRESHOLD = 5
148
149   def __init__(self, manifest_repo_url, directory, branch=None,
150                referenced_repo=None, manifest=constants.DEFAULT_MANIFEST,
151                depth=None, repo_url=constants.REPO_URL, repo_branch=None):
152     self.manifest_repo_url = manifest_repo_url
153     self.repo_url = repo_url
154     self.repo_branch = repo_branch
155     self.directory = directory
156     self.branch = branch
157
158     # It's perfectly acceptable to pass in a reference pathway that isn't
159     # usable.  Detect it, and suppress the setting so that any depth
160     # settings aren't disabled due to it.
161     if referenced_repo is not None:
162       if depth is not None:
163         raise ValueError("referenced_repo and depth are mutually exclusive "
164                          "options; please pick one or the other.")
165       if not IsARepoRoot(referenced_repo):
166         referenced_repo = None
167     self._referenced_repo = referenced_repo
168     self._manifest = manifest
169
170     # If the repo exists already, force a selfupdate as the first step.
171     self._repo_update_needed = IsARepoRoot(self.directory)
172     if not self._repo_update_needed and git.FindRepoDir(self.directory):
173       raise ValueError('Given directory %s is not the root of a repository.'
174                        % self.directory)
175
176     self._depth = int(depth) if depth is not None else None
177
178   def _SwitchToLocalManifest(self, local_manifest):
179     """Reinitializes the repository if the manifest has changed."""
180     logging.debug('Moving to manifest defined by %s', local_manifest)
181     # TODO: use upstream repo's manifest logic when we bump repo version.
182     manifest_path = self.GetRelativePath('.repo/manifest.xml')
183     os.unlink(manifest_path)
184     shutil.copyfile(local_manifest, manifest_path)
185
186   def Initialize(self, local_manifest=None, extra_args=()):
187     """Initializes a repository.  Optionally forces a local manifest.
188
189     Args:
190       local_manifest: The absolute path to a custom manifest to use.  This will
191                       replace .repo/manifest.xml.
192       extra_args: Extra args to pass to 'repo init'
193     """
194
195     # Do a sanity check on the repo; if it exists and we can't pull a
196     # manifest from it, we know it's fairly screwed up and needs a fresh
197     # rebuild.
198     if os.path.exists(os.path.join(self.directory, '.repo', 'manifest.xml')):
199       try:
200         cros_build_lib.RunCommand(
201             ['repo', 'manifest'], cwd=self.directory, capture_output=True)
202       except cros_build_lib.RunCommandError:
203         cros_build_lib.Warning("Wiping %r due to `repo manifest` failure",
204                                self.directory)
205         paths = [os.path.join(self.directory, '.repo', x) for x in
206                  ('manifest.xml', 'manifests.git', 'manifests', 'repo')]
207         cros_build_lib.SudoRunCommand(['rm', '-rf'] + paths)
208         self._repo_update_needed = False
209
210     # Wipe local_manifest.xml if it exists- it can interfere w/ things in
211     # bad ways (duplicate projects, etc); we control this repository, thus
212     # we can destroy it.
213     osutils.SafeUnlink(os.path.join(self.directory, 'local_manifest.xml'))
214
215     # Force a repo self update first; during reinit, repo doesn't do the
216     # update itself, but we could be doing the init on a repo version less
217     # then v1.9.4, which didn't have proper support for doing reinit that
218     # involved changing the manifest branch in use; thus selfupdate.
219     # Additionally, if the self update fails for *any* reason, wipe the repo
220     # innards and force repo init to redownload it; same end result, just
221     # less efficient.
222     # Additionally, note that this method may be called multiple times;
223     # thus code appropriately.
224     if self._repo_update_needed:
225       try:
226         cros_build_lib.RunCommand(['repo', 'selfupdate'], cwd=self.directory)
227       except cros_build_lib.RunCommandError:
228         osutils.RmDir(os.path.join(self.directory, '.repo', 'repo'),
229                       ignore_missing=True)
230       self._repo_update_needed = False
231
232     init_cmd = self._INIT_CMD + ['--repo-url', self.repo_url,
233                                  '--manifest-url', self.manifest_repo_url]
234     if self._referenced_repo:
235       init_cmd.extend(['--reference', self._referenced_repo])
236     if self._manifest:
237       init_cmd.extend(['--manifest-name', self._manifest])
238     if self._depth is not None:
239       init_cmd.extend(['--depth', str(self._depth)])
240     init_cmd.extend(extra_args)
241     # Handle branch / manifest options.
242     if self.branch:
243       init_cmd.extend(['--manifest-branch', self.branch])
244     if self.repo_branch:
245       init_cmd.extend(['--repo-branch', self.repo_branch])
246
247     cros_build_lib.RunCommand(init_cmd, cwd=self.directory, input='\n\ny\n')
248     if local_manifest and local_manifest != self._manifest:
249       self._SwitchToLocalManifest(local_manifest)
250
251   @property
252   def _ManifestConfig(self):
253     return os.path.join(self.directory, '.repo', 'manifests.git', 'config')
254
255   def _EnsureMirroring(self, post_sync=False):
256     """Ensure git is usable from w/in the chroot if --references is enabled
257
258     repo init --references hardcodes the abspath to parent; this pathway
259     however isn't usable from the chroot (it doesn't exist).  As such the
260     pathway is rewritten to use relative pathways pointing at the root of
261     the repo, which via I84988630 enter_chroot sets up a helper bind mount
262     allowing git/repo to access the actual referenced repo.
263
264     This has to be invoked prior to a repo sync of the target trybot to
265     fix any pathways that may have been broken by the parent repo moving
266     on disk, and needs to be invoked after the sync has completed to rewrite
267     any new project's abspath to relative.
268     """
269     if not self._referenced_repo:
270       return
271
272     proj_root = os.path.join(self.directory, '.repo', 'project-objects')
273     if not os.path.exists(proj_root):
274       # Not yet synced, nothing to be done.
275       return
276
277     rewrite_git_alternates.RebuildRepoCheckout(self.directory,
278                                                self._referenced_repo)
279
280     if post_sync:
281       chroot_path = os.path.join(self._referenced_repo, '.repo', 'chroot',
282                                  'external')
283       chroot_path = git.ReinterpretPathForChroot(chroot_path)
284       rewrite_git_alternates.RebuildRepoCheckout(
285           self.directory, self._referenced_repo, chroot_path)
286
287     # Finally, force the git config marker that enter_chroot looks for
288     # to know when to do bind mounting trickery; this normally will exist,
289     # but if we're converting a pre-existing repo checkout, it's possible
290     # that it was invoked w/out the reference arg.  Note this must be
291     # an absolute path to the source repo- enter_chroot uses that to know
292     # what to bind mount into the chroot.
293     cmd = ['config', '--file', self._ManifestConfig, 'repo.reference',
294            self._referenced_repo]
295     git.RunGit('.', cmd)
296
297   def Detach(self):
298     """Detach projects back to manifest versions.  Effectively a 'reset'."""
299     cros_build_lib.RunCommand(['repo', '--time', 'sync', '-d'],
300                               cwd=self.directory)
301
302   def Sync(self, local_manifest=None, jobs=None, all_branches=True,
303            network_only=False):
304     """Sync/update the source.  Changes manifest if specified.
305
306     Args:
307       local_manifest: If true, checks out source to manifest.  DEFAULT_MANIFEST
308         may be used to set it back to the default manifest.
309       jobs: May be set to override the default sync parallelism defined by
310         the manifest.
311       all_branches: If False, a repo sync -c is performed; this saves on
312         sync'ing via grabbing only what is needed for the manifest specified
313         branch. Defaults to True. TODO(davidjames): Set the default back to
314         False once we've fixed http://crbug.com/368722 .
315       network_only: If true, perform only the network half of the sync; skip
316         the checkout.  Primarily of use to validate a manifest (although
317         if the manifest has bad copyfile statements, via skipping checkout
318         the broken copyfile tag won't be spotted), or of use when the
319         invoking code is fine w/ operating on bare repos, ie .repo/projects/*.
320     """
321     try:
322       # Always re-initialize to the current branch.
323       self.Initialize(local_manifest)
324       # Fix existing broken mirroring configurations.
325       self._EnsureMirroring()
326
327       cmd = ['repo', '--time', 'sync']
328       if jobs:
329         cmd += ['--jobs', str(jobs)]
330       if not all_branches:
331         cmd.append('-c')
332       # Do the network half of the sync; retry as necessary to get the content.
333       retry_util.RunCommandWithRetries(constants.SYNC_RETRIES, cmd + ['-n'],
334                                        cwd=self.directory)
335
336       if network_only:
337         return
338
339       # Do the local sync; note that there is a couple of corner cases where
340       # the new manifest cannot transition from the old checkout cleanly-
341       # primarily involving git submodules.  Thus we intercept, and do
342       # a forced wipe, then a retry.
343       try:
344         cros_build_lib.RunCommand(cmd + ['-l'], cwd=self.directory)
345       except cros_build_lib.RunCommandError:
346         manifest = git.ManifestCheckout.Cached(self.directory)
347         targets = set(project['path'].split('/', 1)[0]
348                       for project in manifest.ListCheckouts())
349         if not targets:
350           # No directories to wipe, thus nothing we can fix.
351           raise
352
353         cros_build_lib.SudoRunCommand(['rm', '-rf'] + sorted(targets),
354                                       cwd=self.directory)
355
356         # Retry the sync now; if it fails, let the exception propagate.
357         cros_build_lib.RunCommand(cmd + ['-l'], cwd=self.directory)
358
359       # We do a second run to fix any new repositories created by repo to
360       # use relative object pathways.  Note that cros_sdk also triggers the
361       # same cleanup- we however kick it erring on the side of caution.
362       self._EnsureMirroring(True)
363       self._DoCleanup()
364
365     except cros_build_lib.RunCommandError as e:
366       err_msg = e.Stringify(error=False, output=False)
367       logging.error(err_msg)
368       raise SrcCheckOutException(err_msg)
369
370   def _DoCleanup(self):
371     """Wipe unused repositories."""
372
373     # Find all projects, even if they're not in the manifest.  Note the find
374     # trickery this is done to keep it as fast as possible.
375     repo_path = os.path.join(self.directory, '.repo', 'projects')
376     current = set(cros_build_lib.RunCommand(
377         ['find', repo_path, '-type', 'd', '-name', '*.git', '-printf', '%P\n',
378          '-a', '!', '-wholename',  '*.git/*', '-prune'],
379         print_cmd=False, capture_output=True).output.splitlines())
380     data = {}.fromkeys(current, 0)
381
382     path = os.path.join(self.directory, '.repo', 'project.lru')
383     if os.path.exists(path):
384       existing = [x.strip().split(None, 1)
385                   for x in osutils.ReadFile(path).splitlines()]
386       data.update((k, int(v)) for k, v in existing if k in current)
387
388     # Increment it all...
389     data.update((k, v + 1) for k, v in data.iteritems())
390     # Zero out what is now used.
391     checkouts = git.ManifestCheckout.Cached(self.directory).ListCheckouts()
392     data.update(('%s.git' % x['path'], 0) for x in checkouts)
393
394     # Finally... wipe anything that's greater than our threshold.
395     wipes = [k for k, v in data.iteritems() if v > self.LRU_THRESHOLD]
396     if wipes:
397       cros_build_lib.SudoRunCommand(
398           ['rm', '-rf'] + [os.path.join(repo_path, proj) for proj in wipes])
399       map(data.pop, wipes)
400
401     osutils.WriteFile(path, "\n".join('%s %i' % x for x in data.iteritems()))
402
403   def GetRelativePath(self, path):
404     """Returns full path including source directory of path in repo."""
405     return os.path.join(self.directory, path)
406
407   def ExportManifest(self, mark_revision=False, revisions=True):
408     """Export the revision locked manifest
409
410     Args:
411       mark_revision: If True, then the sha1 of manifest.git is recorded
412         into the resultant manifest tag as a version attribute.
413         Specifically, if manifests.git is at 1234, <manifest> becomes
414         <manifest revision="1234">.
415       revisions: If True, then rewrite all branches/tags into a specific
416         sha1 revision.  If False, don't.
417
418     Returns:
419       The manifest as a string.
420     """
421     cmd = ['repo', 'manifest', '-o', '-']
422     if revisions:
423       cmd += ['-r']
424     output = cros_build_lib.RunCommand(
425         cmd, cwd=self.directory, print_cmd=False, capture_output=True,
426         extra_env={'PAGER':'cat'}).output
427
428     if not mark_revision:
429       return output
430     modified = git.RunGit(os.path.join(self.directory, '.repo/manifests'),
431                           ['rev-list', '-n1', 'HEAD'])
432     assert modified.output
433     return output.replace("<manifest>", '<manifest revision="%s">' %
434                           modified.output.strip())
435
436   def IsManifestDifferent(self, other_manifest):
437     """Checks whether this manifest is different than another.
438
439     May blacklists certain repos as part of the diff.
440
441     Args:
442       other_manifest: Second manifest file to compare against.
443
444     Returns:
445       True: If the manifests are different
446       False: If the manifests are same
447     """
448     logging.debug('Calling IsManifestDifferent against %s', other_manifest)
449
450     black_list = ['="chromium/']
451     blacklist_pattern = re.compile(r'|'.join(black_list))
452     manifest_revision_pattern = re.compile(r'<manifest revision="[a-f0-9]+">',
453                                            re.I)
454
455     current = self.ExportManifest()
456     with open(other_manifest, 'r') as manifest2_fh:
457       for (line1, line2) in zip(current.splitlines(), manifest2_fh):
458         line1 = line1.strip()
459         line2 = line2.strip()
460         if blacklist_pattern.search(line1):
461           logging.debug('%s ignored %s', line1, line2)
462           continue
463
464         if line1 != line2:
465           logging.debug('Current and other manifest differ.')
466           logging.debug('current: "%s"', line1)
467           logging.debug('other  : "%s"', line2)
468
469           # Ignore revision differences on the manifest line. The revision of
470           # the manifest.git repo is uninteresting when determining if the
471           # current manifest describes the same sources as the other manifest.
472           if manifest_revision_pattern.search(line2):
473             logging.debug('Ignoring difference in manifest revision.')
474             continue
475
476           return True
477
478       return False