| # Copyright 2012 The ChromiumOS Authors |
| # Use of this source code is governed by a BSD-style license that can be |
| # found in the LICENSE file. |
| |
| """Functionality for mangling repository checkouts that are shared |
| |
| In particular, this in combination w/ enter_chroot's mount binding, allows |
| us to access the same repo from inside and outside a chroot at the same time |
| """ |
| |
| __all__ = ("RebuildRepoCheckout",) |
| |
| import errno |
| import math |
| import os |
| import shutil |
| |
| from chromite.lib import commandline |
| from chromite.lib import cros_build_lib |
| from chromite.lib import git |
| from chromite.lib import osutils |
| |
| |
| _CACHE_NAME = ".cros_projects.list" |
| |
| |
| def _FilterNonExistentProjects(project_dir, projects): |
| for project in projects: |
| if os.path.exists(os.path.join(project_dir, project)): |
| yield project |
| |
| |
| def _CleanAlternates(projects, alt_root) -> None: |
| alt_root = os.path.normpath(alt_root) |
| |
| projects = set(projects) |
| # Ignore our cache. |
| projects.add(_CACHE_NAME) |
| required_directories = set(os.path.dirname(x) for x in projects) |
| |
| for abs_root, dirs, files in os.walk(alt_root): |
| rel_root = abs_root[len(alt_root) :].strip("/") |
| |
| if rel_root not in required_directories: |
| shutil.rmtree(abs_root) |
| dirs[:] = [] |
| continue |
| |
| if rel_root: |
| for filename in files: |
| if os.path.join(rel_root, filename) not in projects: |
| os.unlink(os.path.join(abs_root, filename)) |
| |
| |
| def _UpdateAlternatesDir(alternates_root, reference_maps, projects) -> None: |
| is_mirror = {} |
| for reference in reference_maps: |
| base = os.path.join(reference, ".repo", "manifests.git") |
| result = git.RunGit( |
| base, ["config", "--local", "--get", "repo.mirror"], check=False |
| ) |
| is_mirror[reference] = ( |
| result.returncode == 0 and result.stdout.strip() == "true" |
| ) |
| |
| for project in projects: |
| alt_path = os.path.join(alternates_root, project) |
| paths = [] |
| for k, v in reference_maps.items(): |
| if is_mirror[k]: |
| # The layout when the reference is a repo mirror (--mirror). |
| suffix = os.path.join(project, "objects") |
| else: |
| # The layout when the reference is a normal repo checkout. |
| suffix = os.path.join( |
| ".repo", "project-objects", project, "objects" |
| ) |
| |
| if os.path.exists(os.path.join(k, suffix)): |
| paths.append(os.path.join(v, suffix)) |
| |
| osutils.SafeMakedirs(os.path.dirname(alt_path)) |
| osutils.WriteFile(alt_path, "%s\n" % ("\n".join(paths),), atomic=True) |
| |
| |
| def _UpdateGitAlternates(proj_root, projects) -> None: |
| for project in projects: |
| alt_path = os.path.join( |
| proj_root, project, "objects", "info", "alternates" |
| ) |
| tmp_path = "%s.tmp" % alt_path |
| |
| # Clean out any tmp files that may have existed prior. |
| osutils.SafeUnlink(tmp_path) |
| |
| # The pathway is written relative to the alternates files absolute path; |
| # literally, .repo/projects/chromite.git/objects/info/alternates. |
| relpath = "../" * (project.count("/") + 4) |
| relpath = os.path.join(relpath, "alternates", project) |
| |
| osutils.SafeMakedirs(os.path.dirname(tmp_path)) |
| os.symlink(relpath, tmp_path) |
| os.rename(tmp_path, alt_path) |
| |
| |
| def _GetProjects(repo_root): |
| # Note that we cannot rely upon projects.list, nor repo list, nor repo |
| # forall here to be authoritative. |
| # if we rely on the manifest contents, the local tree may not yet be |
| # updated - thus if we drop the alternate for that project, that project is |
| # no longer usable (which can tick off repo sync). |
| # Thus, we just iterate over the raw underlying projects store, and generate |
| # alternates for that; we regenerate based on either the manifest changing, |
| # local_manifest having changed, or projects.list having changed (which |
| # occurs during partial local repo syncs; aka repo sync chromite for |
| # example). |
| # Finally, note we have to truncate our mtime awareness to just integers; |
| # this is required since utime isn't guaranteed to set floats, despite our |
| # being able to get a float back from stat'ing. |
| |
| manifest_xml = os.path.join(repo_root, "manifest.xml") |
| times = [os.lstat(manifest_xml).st_mtime, os.stat(manifest_xml).st_mtime] |
| for path in ("local_manifest.xml", "project.list"): |
| path = os.path.join(repo_root, path) |
| if os.path.exists(path): |
| times.append(os.stat(path).st_mtime) |
| |
| # Truncate to seconds. |
| manifest_time = math.trunc(max(times)) |
| |
| cache_path = os.path.join(repo_root, _CACHE_NAME) |
| |
| try: |
| if math.trunc(os.stat(cache_path).st_mtime) == manifest_time: |
| return osutils.ReadFile(cache_path).split() |
| except EnvironmentError as e: |
| if e.errno != errno.ENOENT: |
| raise |
| |
| # The -a ! section of this find invocation is to block descent |
| # into the actual git repository; for IO constrained systems, |
| # this avoids a fairly large amount of inode/dentry load up. |
| # TLDR; It's faster, don't remove it ;) |
| data = cros_build_lib.run( |
| [ |
| "find", |
| "./", |
| "-type", |
| "d", |
| "-name", |
| "*.git", |
| "-a", |
| "!", |
| "-wholename", |
| "*/*.git/*", |
| "-prune", |
| ], |
| cwd=os.path.join(repo_root, "project-objects"), |
| capture_output=True, |
| encoding="utf-8", |
| ) |
| |
| # Drop the leading ./ and the trailing .git |
| data = [x[2:-4] for x in data.stdout.splitlines() if x] |
| |
| with open(cache_path, "w", encoding="utf-8") as f: |
| f.write("\n".join(sorted(data))) |
| |
| # Finally, mark the cache with the time of the manifest.xml we examined. |
| os.utime(cache_path, (manifest_time, manifest_time)) |
| return data |
| |
| |
| class Failed(Exception): |
| """Exception used to fail out for a bad environment.""" |
| |
| |
| def _RebuildRepoCheckout(target_root, reference_map, alternates_dir) -> None: |
| repo_root = os.path.join(target_root, ".repo") |
| proj_root = os.path.join(repo_root, "project-objects") |
| |
| manifest_path = os.path.join(repo_root, "manifest.xml") |
| if not os.path.exists(manifest_path): |
| raise Failed( |
| "%r does not exist, thus cannot be a repo checkout" % manifest_path |
| ) |
| |
| projects = ["%s.git" % x for x in _GetProjects(repo_root)] |
| projects = _FilterNonExistentProjects(proj_root, projects) |
| projects = list(sorted(projects)) |
| |
| if not osutils.SafeMakedirs(alternates_dir, 0o775): |
| # We know the directory exists; thus cleanse out |
| # dead alternates. |
| _CleanAlternates(projects, alternates_dir) |
| |
| _UpdateAlternatesDir(alternates_dir, reference_map, projects) |
| _UpdateGitAlternates(proj_root, projects) |
| |
| |
| def WalkReferences(repo_root, max_depth=5, suppress=()): |
| """Given a repo checkout root, find the repos it references up to max_depth. |
| |
| Args: |
| repo_root: The root of a repo checkout to start from |
| max_depth: Git internally limits the max alternates depth to 5; |
| this option exists to adjust how deep we're willing to look. |
| suppress: List of repos already seen (and so to ignore). |
| |
| Returns: |
| List of repository roots required for this repo_root. |
| """ |
| |
| original_root = repo_root |
| seen = set(os.path.abspath(x) for x in suppress) |
| |
| for _x in range(0, max_depth): |
| repo_root = os.path.abspath(repo_root) |
| |
| if repo_root in seen: |
| # Cyclic reference graph; break out of it, if someone induced this |
| # the necessary objects should be in place. If they aren't, really |
| # isn't much that can be done. |
| return |
| |
| yield repo_root |
| seen.add(repo_root) |
| base = os.path.join(repo_root, ".repo", "manifests.git") |
| result = git.RunGit(base, ["config", "repo.reference"], check=False) |
| |
| if result.returncode not in (0, 1): |
| raise Failed( |
| "Unexpected returncode %i from examining %s git " |
| "repo.reference configuration" % (result.returncode, base) |
| ) |
| |
| repo_root = result.stdout.strip() |
| if not repo_root: |
| break |
| |
| else: |
| raise Failed( |
| "While tracing out the references of %s, we recursed more " |
| "than the allowed %i times ending at %s" |
| % (original_root, max_depth, repo_root) |
| ) |
| |
| |
| def RebuildRepoCheckout( |
| repo_root, initial_reference, chroot_reference_root=None |
| ): |
| """Rebuild a repo checkout's 'alternate tree' rewriting the repo to use it |
| |
| Args: |
| repo_root: absolute path to the root of a repository checkout. |
| initial_reference: absolute path to the root of the repository that is |
| shared. |
| chroot_reference_root: if given, repo_root will have it's chroot |
| alternates tree configured with this pathway, enabling repo access |
| to work from within the chroot. |
| """ |
| |
| reference_roots = list( |
| WalkReferences(initial_reference, suppress=[repo_root]) |
| ) |
| |
| # Always rebuild the external alternates for any operation; 1) we don't want |
| # external out of sync from chroot, 2) if this is the first conversion, if |
| # we only update chroot it'll break external access to the repo. |
| reference_map = dict((x, x) for x in reference_roots) |
| rebuilds = [("alternates", reference_map)] |
| if chroot_reference_root: |
| alternates_dir = "chroot/alternates" |
| base = os.path.join( |
| chroot_reference_root, ".repo", "chroot", "external" |
| ) |
| reference_map = dict( |
| (x, "%s%i" % (base, idx + 1)) |
| for idx, x in enumerate(reference_roots) |
| ) |
| rebuilds += [("chroot/alternates", reference_map)] |
| |
| for alternates_dir, reference_map in rebuilds: |
| alternates_dir = os.path.join(repo_root, ".repo", alternates_dir) |
| _RebuildRepoCheckout(repo_root, reference_map, alternates_dir) |
| return reference_roots |
| |
| |
| def get_parser(): |
| """Return a command line parser""" |
| parser = commandline.ArgumentParser(description=__doc__) |
| parser.add_argument("repository_root", type="str_path") |
| parser.add_argument("referenced_repository", type="str_path") |
| parser.add_argument("chroot_path", type="str_path", nargs="?") |
| return parser |
| |
| |
| def main(argv) -> None: |
| """The main func!""" |
| parser = get_parser() |
| opts = parser.parse_args(argv) |
| opts.Freeze() |
| |
| ret = RebuildRepoCheckout( |
| opts.repository_root, |
| opts.referenced_repository, |
| chroot_reference_root=opts.chroot_path, |
| ) |
| print("\n".join(ret)) |