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.
5 """Functionality for mangling repository checkouts that are shared
7 In particular, this in combination w/ enter_chroot's mount binding, allows
8 us to access the same repo from inside and outside a chroot at the same time
11 from __future__ import print_function
13 __all__ = ('RebuildRepoCheckout',)
20 _path = os.path.realpath(__file__)
21 _path = os.path.normpath(os.path.join(os.path.dirname(_path), '..', '..'))
22 sys.path.insert(0, _path)
25 from chromite.lib import cros_build_lib
26 from chromite.lib import git
27 from chromite.lib import osutils
30 _CACHE_NAME = '.cros_projects.list'
33 def _FilterNonExistentProjects(project_dir, projects):
34 for project in projects:
35 if os.path.exists(os.path.join(project_dir, project)):
39 def _CleanAlternates(projects, alt_root):
40 alt_root = os.path.normpath(alt_root)
42 projects = set(projects)
44 projects.add(_CACHE_NAME)
45 required_directories = set(os.path.dirname(x) for x in projects)
47 for abs_root, dirs, files in os.walk(alt_root):
48 rel_root = abs_root[len(alt_root):].strip('/')
50 if rel_root not in required_directories:
51 shutil.rmtree(abs_root)
56 for filename in files:
57 if os.path.join(rel_root, filename) not in projects:
58 os.unlink(os.path.join(abs_root, filename))
61 def _UpdateAlternatesDir(alternates_root, reference_maps, projects):
62 for project in projects:
63 alt_path = os.path.join(alternates_root, project)
65 for k, v in reference_maps.iteritems():
66 suffix = os.path.join('.repo', 'project-objects', project, 'objects')
67 if os.path.exists(os.path.join(k, suffix)):
68 paths.append(os.path.join(v, suffix))
70 osutils.SafeMakedirs(os.path.dirname(alt_path))
71 osutils.WriteFile(alt_path, '%s\n' % ('\n'.join(paths),), atomic=True)
74 def _UpdateGitAlternates(proj_root, projects):
75 for project in projects:
77 alt_path = os.path.join(proj_root, project, 'objects', 'info',
79 tmp_path = '%s.tmp' % alt_path
81 # Clean out any tmp files that may have existed prior.
82 osutils.SafeUnlink(tmp_path)
84 # The pathway is written relative to the alternates files absolute path;
85 # literally, .repo/projects/chromite.git/objects/info/alternates.
86 relpath = '../' * (project.count('/') + 4)
87 relpath = os.path.join(relpath, 'alternates', project)
89 osutils.SafeMakedirs(os.path.dirname(tmp_path))
90 os.symlink(relpath , tmp_path)
91 os.rename(tmp_path, alt_path)
94 def _GetProjects(repo_root):
95 # Note that we cannot rely upon projects.list, nor repo list, nor repo forall
96 # here to be authoritive.
97 # if we rely on the manifest contents, the local tree may not yet be
98 # updated- thus if we drop the alternate for that project, that project is no
99 # longer usable (which can tick off repo sync).
100 # Thus, we just iterate over the raw underlying projects store, and generate
101 # alternates for that; we regenerate based on either the manifest changing,
102 # local_manifest having changed, or projects.list having changed (which
103 # occurs during partial local repo syncs; aka repo sync chromite for
105 # Finally, note we have to truncate our mtime awareness to just integers;
106 # this is required since utime isn't guaranteed to set floats, despite our
107 # being able to get a float back from stat'ing.
109 manifest_xml = os.path.join(repo_root, 'manifest.xml')
110 times = [os.lstat(manifest_xml).st_mtime,
111 os.stat(manifest_xml).st_mtime]
112 for path in ('local_manifest.xml', 'project.list'):
113 path = os.path.join(repo_root, path)
114 if os.path.exists(path):
115 times.append(os.stat(path).st_mtime)
117 manifest_time = long(max(times))
119 cache_path = os.path.join(repo_root, _CACHE_NAME)
122 if long(os.stat(cache_path).st_mtime) == manifest_time:
123 return osutils.ReadFile(cache_path).split()
124 except EnvironmentError as e:
125 if e.errno != errno.ENOENT:
128 # The -a ! section of this find invocation is to block descent
129 # into the actual git repository; for IO constrained systems,
130 # this avoids a fairly large amount of inode/dentry load up.
131 # TLDR; It's faster, don't remove it ;)
132 data = cros_build_lib.RunCommand(
133 ['find', './', '-type', 'd', '-name', '*.git', '-a',
134 '!', '-wholename', '*/*.git/*', '-prune'],
135 cwd=os.path.join(repo_root, 'project-objects'), capture_output=True)
137 # Drop the leading ./ and the trailing .git
138 data = [x[2:-4] for x in data.output.splitlines() if x]
140 with open(cache_path, 'w') as f:
141 f.write('\n'.join(sorted(data)))
143 # Finally, mark the cache with the time of the manifest.xml we examined.
144 os.utime(cache_path, (manifest_time, manifest_time))
148 class Failed(Exception):
149 """Exception used to fail out for a bad environment."""
152 def _RebuildRepoCheckout(target_root, reference_map,
154 repo_root = os.path.join(target_root, '.repo')
155 proj_root = os.path.join(repo_root, 'project-objects')
157 manifest_path = os.path.join(repo_root, 'manifest.xml')
158 if not os.path.exists(manifest_path):
159 raise Failed('%r does not exist, thus cannot be a repo checkout' %
162 projects = ['%s.git' % x for x in _GetProjects(repo_root)]
163 projects = _FilterNonExistentProjects(proj_root, projects)
164 projects = list(sorted(projects))
166 if not osutils.SafeMakedirs(alternates_dir, 0o775):
167 # We know the directory exists; thus cleanse out
169 _CleanAlternates(projects, alternates_dir)
171 _UpdateAlternatesDir(alternates_dir, reference_map, projects)
172 _UpdateGitAlternates(proj_root, projects)
175 def WalkReferences(repo_root, max_depth=5, suppress=()):
176 """Given a repo checkout root, find the repo's it references up to max_depth.
179 repo_root: The root of a repo checkout to start from
180 max_depth: Git internally limits the max alternates depth to 5;
181 this option exists to adjust how deep we're willing to look.
182 suppress: List of repos already seen (and so to ignore).
185 List of repository roots required for this repo_root.
188 original_root = repo_root
189 seen = set(os.path.abspath(x) for x in suppress)
191 for _x in xrange(0, max_depth):
192 repo_root = os.path.abspath(repo_root)
194 if repo_root in seen:
195 # Cyclic reference graph; break out of it, if someone induced this the
196 # necessary objects should be in place. If they aren't, really isn't
197 # much that can be done.
202 base = os.path.join(repo_root, '.repo', 'manifests.git')
204 base, ['config', 'repo.reference'], error_code_ok=True)
206 if result.returncode not in (0, 1):
207 raise Failed('Unexpected returncode %i from examining %s git '
208 'repo.reference configuration' %
209 (result.returncode, base))
211 repo_root = result.output.strip()
216 raise Failed('While tracing out the references of %s, we recursed more '
217 'than the allowed %i times ending at %s'
218 % (original_root, max_depth, repo_root))
221 def RebuildRepoCheckout(repo_root, initial_reference,
222 chroot_reference_root=None):
223 """Rebuild a repo checkout's 'alternate tree' rewriting the repo to use it
226 repo_root: absolute path to the root of a repository checkout
227 initial_reference: absolute path to the root of the repository that is
229 chroot_reference_root: if given, repo_root will have it's chroot
230 alternates tree configured with this pathway, enabling repo access to
231 work from within the chroot.
234 reference_roots = list(WalkReferences(initial_reference,
235 suppress=[repo_root]))
237 # Always rebuild the external alternates for any operation; 1) we don't want
238 # external out of sync from chroot, 2) if this is the first conversion, if
239 # we only update chroot it'll break external access to the repo.
240 reference_map = dict((x, x) for x in reference_roots)
241 rebuilds = [('alternates', reference_map)]
242 if chroot_reference_root:
243 alternates_dir = 'chroot/alternates'
244 base = os.path.join(chroot_reference_root, '.repo', 'chroot', 'external')
245 reference_map = dict((x, '%s%i' % (base, idx + 1))
246 for idx, x in enumerate(reference_roots))
247 rebuilds += [('chroot/alternates', reference_map)]
249 for alternates_dir, reference_map in rebuilds:
250 alternates_dir = os.path.join(repo_root, '.repo', alternates_dir)
251 _RebuildRepoCheckout(repo_root,
254 return reference_roots
257 if __name__ == '__main__':
259 if len(sys.argv) not in (3, 4):
260 sys.stderr.write('Usage: %s <repository_root> <referenced_repository> '
261 '[path_from_within_the_chroot]\n' % (sys.argv[0],))
263 if len(sys.argv) == 4:
264 chroot_root = sys.argv[3]
265 ret = RebuildRepoCheckout(sys.argv[1], sys.argv[2],
266 chroot_reference_root=chroot_root)
267 print('\n'.join(ret))