Laszlo Csomor | c77f891 | 2017-09-05 09:35:39 +0200 | [diff] [blame] | 1 | # pylint: disable=g-direct-third-party-import |
| 2 | # Copyright 2017 The Bazel Authors. All rights reserved. |
| 3 | # |
| 4 | # Licensed under the Apache License, Version 2.0 (the "License"); |
| 5 | # you may not use this file except in compliance with the License. |
| 6 | # You may obtain a copy of the License at |
| 7 | # |
| 8 | # http://www.apache.org/licenses/LICENSE-2.0 |
| 9 | # |
| 10 | # Unless required by applicable law or agreed to in writing, software |
| 11 | # distributed under the License is distributed on an "AS IS" BASIS, |
| 12 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| 13 | # See the License for the specific language governing permissions and |
| 14 | # limitations under the License. |
| 15 | """A class that creates junctions in temp directories on Windows. |
| 16 | |
| 17 | Only use this class on Windows, do not use on other platforms. Other platforms |
| 18 | support longer paths than Windows, and also support symlinks. Windows only |
| 19 | supports junctions (directory symlinks). |
| 20 | |
| 21 | Junctions are useful if you need to shorten a long path. A long path is one that |
| 22 | is at least MAX_PATH (260) letters long. This is a constant in Windows, see |
| 23 | <windows.h> and API documentation for file-handling functions such as |
| 24 | CreateFileA. |
| 25 | """ |
| 26 | |
| 27 | import os |
| 28 | import subprocess |
| 29 | import tempfile |
| 30 | |
| 31 | |
| 32 | class JunctionCreationError(Exception): |
| 33 | """Raised when TempJunction fails to create an NTFS junction.""" |
| 34 | |
| 35 | def __init__(self, path, target, stdout): |
| 36 | Exception.__init__( |
| 37 | self, |
| 38 | "Could not create junction \"%s\" -> \"%s\"\nError from mklink:\n%s" % |
| 39 | (path, target, stdout)) |
| 40 | |
| 41 | |
| 42 | class TempJunction(object): |
| 43 | r"""Junction in a temp directory. |
| 44 | |
| 45 | This object creates a temp directory and a junction under it. The junction |
| 46 | points to a user-specified path. |
| 47 | |
| 48 | Use this object if you want to write files under long paths (absolute path at |
| 49 | least MAX_PATH (260) chars long). Pass the directory you want to "shorten" as |
| 50 | the initializer's argument. This object will create a junction under a shorter |
| 51 | path, that points to the long directory. The combined path of the junction and |
| 52 | files under it are more likely to be short than the original paths were. |
| 53 | |
| 54 | Usage example: |
| 55 | with TempJunction("C:/some/long/path") as junc: |
| 56 | # `junc` created a temp directory and a junction in it that points to |
| 57 | # \\?\C:\some\long\path, and is itself shorter than that |
| 58 | shortpath = os.path.join(junc, "file.txt") |
| 59 | with open(shortpath, "w") as f: |
| 60 | ...do something with f... |
| 61 | # `junc` deleted the junction and its parent temp directory upon leaving |
| 62 | # the `with` statement's body |
| 63 | ...do something else... |
| 64 | """ |
| 65 | |
Laszlo Csomor | ec91d20 | 2017-09-27 06:17:44 -0400 | [diff] [blame] | 66 | def __init__(self, |
| 67 | junction_target, |
| 68 | testonly_mkdtemp=None, |
| 69 | testonly_maxpath=None): |
Laszlo Csomor | c77f891 | 2017-09-05 09:35:39 +0200 | [diff] [blame] | 70 | """Initialize this object. |
| 71 | |
| 72 | Args: |
| 73 | junction_target: string; an absolute Windows path; the __enter__ method |
| 74 | creates a junction that points to this path |
| 75 | testonly_mkdtemp: function(); for testing only; a custom function that |
| 76 | returns a temp directory path, you can use it to mock out |
| 77 | tempfile.mkdtemp |
Laszlo Csomor | ec91d20 | 2017-09-27 06:17:44 -0400 | [diff] [blame] | 78 | testonly_maxpath: int; for testing oly; maximum path length before the |
| 79 | path is a "long path" (typically MAX_PATH on Windows) |
Laszlo Csomor | c77f891 | 2017-09-05 09:35:39 +0200 | [diff] [blame] | 80 | """ |
Laszlo Csomor | ec91d20 | 2017-09-27 06:17:44 -0400 | [diff] [blame] | 81 | self._target = os.path.abspath(junction_target) |
Laszlo Csomor | c77f891 | 2017-09-05 09:35:39 +0200 | [diff] [blame] | 82 | self._junction = None |
| 83 | self._mkdtemp = testonly_mkdtemp or tempfile.mkdtemp |
Laszlo Csomor | ec91d20 | 2017-09-27 06:17:44 -0400 | [diff] [blame] | 84 | self._max_path = testonly_maxpath or 248 |
| 85 | |
| 86 | @staticmethod |
| 87 | def _Mklink(name, target): |
| 88 | proc = subprocess.Popen( |
| 89 | "cmd.exe /C mklink /J \"%s\" \"\\\\?\\%s\"" % (name, target), |
| 90 | stdout=subprocess.PIPE, |
| 91 | stderr=subprocess.STDOUT) |
| 92 | exitcode = proc.wait() |
| 93 | if exitcode != 0: |
| 94 | stdout = proc.communicate()[0] |
| 95 | raise JunctionCreationError(name, target, stdout) |
| 96 | |
| 97 | @staticmethod |
| 98 | def _TryMkdir(path): |
| 99 | try: |
| 100 | os.mkdir(path) |
| 101 | except OSError as e: |
| 102 | # Another process may have already created this directory. |
| 103 | if not os.path.isdir(path): |
| 104 | raise IOError("Could not create directory at '%s': %s" % (path, str(e))) |
| 105 | |
| 106 | @staticmethod |
| 107 | def _MakeLinks(target, mkdtemp, max_path): |
| 108 | """Creates a temp directory and a junction in it, pointing to `target`. |
| 109 | |
| 110 | Creates all parent directories of `target` if they don't exist. |
| 111 | |
| 112 | Args: |
| 113 | target: string; path to the directory that is the junction's target |
| 114 | mkdtemp: function():string; creates a temp directory and returns its |
| 115 | absolute path |
| 116 | max_path: int; maximum path length before the path is a "long path" |
| 117 | (typically MAX_PATH on Windows) |
| 118 | Returns: |
| 119 | The full path to the junction. |
| 120 | Raises: |
| 121 | JunctionCreationError: if `mklink` fails to create a junction |
| 122 | """ |
| 123 | segments = [] |
| 124 | dirpath = target |
| 125 | while not os.path.isdir(dirpath): |
| 126 | dirpath, child = os.path.split(dirpath) |
| 127 | if child: |
| 128 | segments.append(child) |
| 129 | tmp = mkdtemp() |
| 130 | juncpath = os.path.join(tmp, "j") |
| 131 | for child in reversed(segments): |
| 132 | childpath = os.path.join(dirpath, child) |
| 133 | if len(childpath) >= max_path: |
| 134 | try: |
| 135 | TempJunction._Mklink(juncpath, dirpath) |
| 136 | TempJunction._TryMkdir(os.path.join(juncpath, child)) |
| 137 | finally: |
| 138 | os.rmdir(juncpath) |
| 139 | else: |
| 140 | TempJunction._TryMkdir(childpath) |
| 141 | dirpath = childpath |
| 142 | TempJunction._Mklink(juncpath, target) |
| 143 | return juncpath |
Laszlo Csomor | c77f891 | 2017-09-05 09:35:39 +0200 | [diff] [blame] | 144 | |
| 145 | def __enter__(self): |
| 146 | """Creates a temp directory and a junction in it, pointing to self._target. |
| 147 | |
Laszlo Csomor | ec91d20 | 2017-09-27 06:17:44 -0400 | [diff] [blame] | 148 | Creates all parent directories of self._target if they don't exist. |
| 149 | |
Laszlo Csomor | c77f891 | 2017-09-05 09:35:39 +0200 | [diff] [blame] | 150 | This method is automatically called upon entering a `with` statement's body. |
| 151 | |
| 152 | Returns: |
| 153 | The full path to the junction. |
| 154 | Raises: |
| 155 | JunctionCreationError: if `mklink` fails to create a junction |
| 156 | """ |
Laszlo Csomor | ec91d20 | 2017-09-27 06:17:44 -0400 | [diff] [blame] | 157 | self._junction = TempJunction._MakeLinks(self._target, self._mkdtemp, |
| 158 | self._max_path) |
| 159 | return self._junction |
Laszlo Csomor | c77f891 | 2017-09-05 09:35:39 +0200 | [diff] [blame] | 160 | |
| 161 | def __exit__(self, unused_type, unused_value, unused_traceback): |
| 162 | """Deletes the junction and its parent directory. |
| 163 | |
| 164 | This method is automatically called upon leaving a `with` statement's body. |
| 165 | |
| 166 | Args: |
| 167 | unused_type: unused |
| 168 | unused_value: unused |
| 169 | unused_traceback: unused |
| 170 | """ |
| 171 | if self._junction: |
| 172 | os.rmdir(self._junction) |
| 173 | os.rmdir(os.path.dirname(self._junction)) |