Upstream version 10.39.225.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     repo_url: gitserver 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   """
141   # Use our own repo, in case android.kernel.org (the default location) is down.
142   _INIT_CMD = ['repo', 'init', '--repo-url', constants.REPO_URL]
143
144   # If a repo hasn't been used in the last 5 runs, wipe it.
145   LRU_THRESHOLD = 5
146
147   def __init__(self, repo_url, directory, branch=None, referenced_repo=None,
148                manifest=constants.DEFAULT_MANIFEST, depth=None):
149     self.repo_url = repo_url
150     self.directory = directory
151     self.branch = branch
152
153     # It's perfectly acceptable to pass in a reference pathway that isn't
154     # usable.  Detect it, and suppress the setting so that any depth
155     # settings aren't disabled due to it.
156     if referenced_repo is not None:
157       if depth is not None:
158         raise ValueError("referenced_repo and depth are mutually exclusive "
159                          "options; please pick one or the other.")
160       if not IsARepoRoot(referenced_repo):
161         referenced_repo = None
162     self._referenced_repo = referenced_repo
163     self._manifest = manifest
164
165     # If the repo exists already, force a selfupdate as the first step.
166     self._repo_update_needed = IsARepoRoot(self.directory)
167     if not self._repo_update_needed and git.FindRepoDir(self.directory):
168       raise ValueError('Given directory %s is not the root of a repository.'
169                        % self.directory)
170
171     self._depth = int(depth) if depth is not None else None
172
173   def _SwitchToLocalManifest(self, local_manifest):
174     """Reinitializes the repository if the manifest has changed."""
175     logging.debug('Moving to manifest defined by %s', local_manifest)
176     # TODO: use upstream repo's manifest logic when we bump repo version.
177     manifest_path = self.GetRelativePath('.repo/manifest.xml')
178     os.unlink(manifest_path)
179     shutil.copyfile(local_manifest, manifest_path)
180
181   def Initialize(self, local_manifest=None, extra_args=()):
182     """Initializes a repository.  Optionally forces a local manifest.
183
184     Args:
185       local_manifest: The absolute path to a custom manifest to use.  This will
186                       replace .repo/manifest.xml.
187       extra_args: Extra args to pass to 'repo init'
188     """
189
190     # Do a sanity check on the repo; if it exists and we can't pull a
191     # manifest from it, we know it's fairly screwed up and needs a fresh
192     # rebuild.
193     if os.path.exists(os.path.join(self.directory, '.repo', 'manifest.xml')):
194       try:
195         cros_build_lib.RunCommand(
196             ['repo', 'manifest'], cwd=self.directory, capture_output=True)
197       except cros_build_lib.RunCommandError:
198         cros_build_lib.Warning("Wiping %r due to `repo manifest` failure",
199                                self.directory)
200         paths = [os.path.join(self.directory, '.repo', x) for x in
201                  ('manifest.xml', 'manifests.git', 'manifests', 'repo')]
202         cros_build_lib.SudoRunCommand(['rm', '-rf'] + paths)
203         self._repo_update_needed = False
204
205     # Wipe local_manifest.xml if it exists- it can interfere w/ things in
206     # bad ways (duplicate projects, etc); we control this repository, thus
207     # we can destroy it.
208     osutils.SafeUnlink(os.path.join(self.directory, 'local_manifest.xml'))
209
210     # Force a repo self update first; during reinit, repo doesn't do the
211     # update itself, but we could be doing the init on a repo version less
212     # then v1.9.4, which didn't have proper support for doing reinit that
213     # involved changing the manifest branch in use; thus selfupdate.
214     # Additionally, if the self update fails for *any* reason, wipe the repo
215     # innards and force repo init to redownload it; same end result, just
216     # less efficient.
217     # Additionally, note that this method may be called multiple times;
218     # thus code appropriately.
219     if self._repo_update_needed:
220       try:
221         cros_build_lib.RunCommand(['repo', 'selfupdate'], cwd=self.directory)
222       except cros_build_lib.RunCommandError:
223         osutils.RmDir(os.path.join(self.directory, '.repo', 'repo'),
224                       ignore_missing=True)
225       self._repo_update_needed = False
226
227     init_cmd = self._INIT_CMD + ['--manifest-url', self.repo_url]
228     if self._referenced_repo:
229       init_cmd.extend(['--reference', self._referenced_repo])
230     if self._manifest:
231       init_cmd.extend(['--manifest-name', self._manifest])
232     if self._depth is not None:
233       init_cmd.extend(['--depth', str(self._depth)])
234     init_cmd.extend(extra_args)
235     # Handle branch / manifest options.
236     if self.branch:
237       init_cmd.extend(['--manifest-branch', self.branch])
238
239     cros_build_lib.RunCommand(init_cmd, cwd=self.directory, input='\n\ny\n')
240     if local_manifest and local_manifest != self._manifest:
241       self._SwitchToLocalManifest(local_manifest)
242
243   @property
244   def _ManifestConfig(self):
245     return os.path.join(self.directory, '.repo', 'manifests.git', 'config')
246
247   def _EnsureMirroring(self, post_sync=False):
248     """Ensure git is usable from w/in the chroot if --references is enabled
249
250     repo init --references hardcodes the abspath to parent; this pathway
251     however isn't usable from the chroot (it doesn't exist).  As such the
252     pathway is rewritten to use relative pathways pointing at the root of
253     the repo, which via I84988630 enter_chroot sets up a helper bind mount
254     allowing git/repo to access the actual referenced repo.
255
256     This has to be invoked prior to a repo sync of the target trybot to
257     fix any pathways that may have been broken by the parent repo moving
258     on disk, and needs to be invoked after the sync has completed to rewrite
259     any new project's abspath to relative.
260     """
261     if not self._referenced_repo:
262       return
263
264     proj_root = os.path.join(self.directory, '.repo', 'project-objects')
265     if not os.path.exists(proj_root):
266       # Not yet synced, nothing to be done.
267       return
268
269     rewrite_git_alternates.RebuildRepoCheckout(self.directory,
270                                                self._referenced_repo)
271
272     if post_sync:
273       chroot_path = os.path.join(self._referenced_repo, '.repo', 'chroot',
274                                  'external')
275       chroot_path = git.ReinterpretPathForChroot(chroot_path)
276       rewrite_git_alternates.RebuildRepoCheckout(
277           self.directory, self._referenced_repo, chroot_path)
278
279     # Finally, force the git config marker that enter_chroot looks for
280     # to know when to do bind mounting trickery; this normally will exist,
281     # but if we're converting a pre-existing repo checkout, it's possible
282     # that it was invoked w/out the reference arg.  Note this must be
283     # an absolute path to the source repo- enter_chroot uses that to know
284     # what to bind mount into the chroot.
285     cmd = ['config', '--file', self._ManifestConfig, 'repo.reference',
286            self._referenced_repo]
287     git.RunGit('.', cmd)
288
289   def Detach(self):
290     """Detach projects back to manifest versions.  Effectively a 'reset'."""
291     cros_build_lib.RunCommand(['repo', '--time', 'sync', '-d'],
292                               cwd=self.directory)
293
294   def Sync(self, local_manifest=None, jobs=None, all_branches=True,
295            network_only=False):
296     """Sync/update the source.  Changes manifest if specified.
297
298     Args:
299       local_manifest: If true, checks out source to manifest.  DEFAULT_MANIFEST
300         may be used to set it back to the default manifest.
301       jobs: May be set to override the default sync parallelism defined by
302         the manifest.
303       all_branches: If False, a repo sync -c is performed; this saves on
304         sync'ing via grabbing only what is needed for the manifest specified
305         branch. Defaults to True. TODO(davidjames): Set the default back to
306         False once we've fixed http://crbug.com/368722 .
307       network_only: If true, perform only the network half of the sync; skip
308         the checkout.  Primarily of use to validate a manifest (although
309         if the manifest has bad copyfile statements, via skipping checkout
310         the broken copyfile tag won't be spotted), or of use when the
311         invoking code is fine w/ operating on bare repos, ie .repo/projects/*.
312     """
313     try:
314       # Always re-initialize to the current branch.
315       self.Initialize(local_manifest)
316       # Fix existing broken mirroring configurations.
317       self._EnsureMirroring()
318
319       cmd = ['repo', '--time', 'sync']
320       if jobs:
321         cmd += ['--jobs', str(jobs)]
322       if not all_branches:
323         cmd.append('-c')
324       # Do the network half of the sync; retry as necessary to get the content.
325       retry_util.RunCommandWithRetries(constants.SYNC_RETRIES, cmd + ['-n'],
326                                        cwd=self.directory)
327
328       if network_only:
329         return
330
331       # Do the local sync; note that there is a couple of corner cases where
332       # the new manifest cannot transition from the old checkout cleanly-
333       # primarily involving git submodules.  Thus we intercept, and do
334       # a forced wipe, then a retry.
335       try:
336         cros_build_lib.RunCommand(cmd + ['-l'], cwd=self.directory)
337       except cros_build_lib.RunCommandError:
338         manifest = git.ManifestCheckout.Cached(self.directory)
339         targets = set(project['path'].split('/', 1)[0]
340                       for project in manifest.ListCheckouts())
341         if not targets:
342           # No directories to wipe, thus nothing we can fix.
343           raise
344
345         cros_build_lib.SudoRunCommand(['rm', '-rf'] + sorted(targets),
346                                       cwd=self.directory)
347
348         # Retry the sync now; if it fails, let the exception propagate.
349         cros_build_lib.RunCommand(cmd + ['-l'], cwd=self.directory)
350
351       # We do a second run to fix any new repositories created by repo to
352       # use relative object pathways.  Note that cros_sdk also triggers the
353       # same cleanup- we however kick it erring on the side of caution.
354       self._EnsureMirroring(True)
355       self._DoCleanup()
356
357     except cros_build_lib.RunCommandError as e:
358       err_msg = e.Stringify(error=False, output=False)
359       logging.error(err_msg)
360       raise SrcCheckOutException(err_msg)
361
362   def _DoCleanup(self):
363     """Wipe unused repositories."""
364
365     # Find all projects, even if they're not in the manifest.  Note the find
366     # trickery this is done to keep it as fast as possible.
367     repo_path = os.path.join(self.directory, '.repo', 'projects')
368     current = set(cros_build_lib.RunCommand(
369         ['find', repo_path, '-type', 'd', '-name', '*.git', '-printf', '%P\n',
370          '-a', '!', '-wholename',  '*.git/*', '-prune'],
371         print_cmd=False, capture_output=True).output.splitlines())
372     data = {}.fromkeys(current, 0)
373
374     path = os.path.join(self.directory, '.repo', 'project.lru')
375     if os.path.exists(path):
376       existing = [x.strip().split(None, 1)
377                   for x in osutils.ReadFile(path).splitlines()]
378       data.update((k, int(v)) for k, v in existing if k in current)
379
380     # Increment it all...
381     data.update((k, v + 1) for k, v in data.iteritems())
382     # Zero out what is now used.
383     checkouts = git.ManifestCheckout.Cached(self.directory).ListCheckouts()
384     data.update(('%s.git' % x['path'], 0) for x in checkouts)
385
386     # Finally... wipe anything that's greater than our threshold.
387     wipes = [k for k, v in data.iteritems() if v > self.LRU_THRESHOLD]
388     if wipes:
389       cros_build_lib.SudoRunCommand(
390           ['rm', '-rf'] + [os.path.join(repo_path, proj) for proj in wipes])
391       map(data.pop, wipes)
392
393     osutils.WriteFile(path, "\n".join('%s %i' % x for x in data.iteritems()))
394
395   def GetRelativePath(self, path):
396     """Returns full path including source directory of path in repo."""
397     return os.path.join(self.directory, path)
398
399   def ExportManifest(self, mark_revision=False, revisions=True):
400     """Export the revision locked manifest
401
402     Args:
403       mark_revision: If True, then the sha1 of manifest.git is recorded
404         into the resultant manifest tag as a version attribute.
405         Specifically, if manifests.git is at 1234, <manifest> becomes
406         <manifest revision="1234">.
407       revisions: If True, then rewrite all branches/tags into a specific
408         sha1 revision.  If False, don't.
409
410     Returns:
411       The manifest as a string.
412     """
413     cmd = ['repo', 'manifest', '-o', '-']
414     if revisions:
415       cmd += ['-r']
416     output = cros_build_lib.RunCommand(
417         cmd, cwd=self.directory, print_cmd=False, capture_output=True,
418         extra_env={'PAGER':'cat'}).output
419
420     if not mark_revision:
421       return output
422     modified = git.RunGit(os.path.join(self.directory, '.repo/manifests'),
423                           ['rev-list', '-n1', 'HEAD'])
424     assert modified.output
425     return output.replace("<manifest>", '<manifest revision="%s">' %
426                           modified.output.strip())
427
428   def IsManifestDifferent(self, other_manifest):
429     """Checks whether this manifest is different than another.
430
431     May blacklists certain repos as part of the diff.
432
433     Args:
434       other_manifest: Second manifest file to compare against.
435
436     Returns:
437       True: If the manifests are different
438       False: If the manifests are same
439     """
440     logging.debug('Calling IsManifestDifferent against %s', other_manifest)
441
442     black_list = ['="chromium/']
443     blacklist_pattern = re.compile(r'|'.join(black_list))
444     manifest_revision_pattern = re.compile(r'<manifest revision="[a-f0-9]+">',
445                                            re.I)
446
447     current = self.ExportManifest()
448     with open(other_manifest, 'r') as manifest2_fh:
449       for (line1, line2) in zip(current.splitlines(), manifest2_fh):
450         line1 = line1.strip()
451         line2 = line2.strip()
452         if blacklist_pattern.search(line1):
453           logging.debug('%s ignored %s', line1, line2)
454           continue
455
456         if line1 != line2:
457           logging.debug('Current and other manifest differ.')
458           logging.debug('current: "%s"', line1)
459           logging.debug('other  : "%s"', line2)
460
461           # Ignore revision differences on the manifest line. The revision of
462           # the manifest.git repo is uninteresting when determining if the
463           # current manifest describes the same sources as the other manifest.
464           if manifest_revision_pattern.search(line2):
465             logging.debug('Ignoring difference in manifest revision.')
466             continue
467
468           return True
469
470       return False