xref: /llvm-project/utils/bazel/overlay_directories.py (revision f98ee40f4b5d7474fc67e82824bf6abbaedb7b1c)
1#!/bin/python3
2
3# This file is licensed under the Apache License v2.0 with LLVM Exceptions.
4# See https://llvm.org/LICENSE.txt for license information.
5# SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
6"""Overlays two directories into a target directory using symlinks.
7
8Tries to minimize the number of symlinks created (that is, does not symlink
9every single file). Symlinks every file in the overlay directory. Only symlinks
10individual files in the source directory if their parent directory is also
11contained in the overlay directory tree.
12"""
13
14import argparse
15import errno
16import os
17import sys
18
19
20def _check_python_version():
21    if sys.version_info[0] < 3:
22        raise RuntimeError(
23            "Must be invoked with a python 3 interpreter but was %s" % sys.executable
24        )
25
26
27def _check_dir_exists(path):
28    if not os.path.isdir(path):
29        raise OSError(errno.ENOENT, os.strerror(errno.ENOENT), path)
30
31
32def parse_arguments():
33    parser = argparse.ArgumentParser(
34        description="""
35    Overlays two directories into a target directory using symlinks.
36
37    Tries to minimize the number of symlinks created (that is, does not symlink
38    every single file). Symlinks every file in the overlay directory. Only
39    symlinks individual files in the source directory if their parent directory
40    is also contained in the overlay directory tree.
41    """
42    )
43    parser.add_argument(
44        "--src",
45        required=True,
46        help="Directory that contains most of the content to symlink.",
47    )
48    parser.add_argument(
49        "--overlay",
50        required=True,
51        help="Directory to overlay on top of the source directory.",
52    )
53    parser.add_argument(
54        "--target",
55        required=True,
56        help="Directory in which to place the fused symlink directories.",
57    )
58
59    args = parser.parse_args()
60
61    _check_dir_exists(args.target)
62    _check_dir_exists(args.overlay)
63    _check_dir_exists(args.src)
64
65    return args
66
67
68def _symlink_abs(from_path, to_path):
69    os.symlink(os.path.abspath(from_path), os.path.abspath(to_path))
70
71
72def main(args):
73    for root, dirs, files in os.walk(args.overlay):
74        # We could do something more intelligent here and only symlink individual
75        # files if the directory is present in both overlay and src. This could also
76        # be generalized to an arbitrary number of directories without any
77        # "src/overlay" distinction. In the current use case we only have two and
78        # the overlay directory is always small, so putting that off for now.
79        rel_root = os.path.relpath(root, start=args.overlay)
80        if rel_root != ".":
81            os.mkdir(os.path.join(args.target, rel_root))
82
83        for file in files:
84            relpath = os.path.join(rel_root, file)
85            _symlink_abs(
86                os.path.join(args.overlay, relpath), os.path.join(args.target, relpath)
87            )
88
89        for src_entry in os.listdir(os.path.join(args.src, rel_root)):
90            if src_entry not in dirs:
91                relpath = os.path.join(rel_root, src_entry)
92                _symlink_abs(
93                    os.path.join(args.src, relpath), os.path.join(args.target, relpath)
94                )
95
96
97if __name__ == "__main__":
98    _check_python_version()
99    main(parse_arguments())
100