| # pylint: disable=g-direct-third-party-import |
| # Copyright 2017 The Bazel Authors. All rights reserved. |
| # |
| # Licensed under the Apache License, Version 2.0 (the "License"); |
| # you may not use this file except in compliance with the License. |
| # You may obtain a copy of the License at |
| # |
| # http://www.apache.org/licenses/LICENSE-2.0 |
| # |
| # Unless required by applicable law or agreed to in writing, software |
| # distributed under the License is distributed on an "AS IS" BASIS, |
| # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| # See the License for the specific language governing permissions and |
| # limitations under the License. |
| """A class that creates junctions in temp directories on Windows. |
| |
| Only use this class on Windows, do not use on other platforms. Other platforms |
| support longer paths than Windows, and also support symlinks. Windows only |
| supports junctions (directory symlinks). |
| |
| Junctions are useful if you need to shorten a long path. A long path is one that |
| is at least MAX_PATH (260) letters long. This is a constant in Windows, see |
| <windows.h> and API documentation for file-handling functions such as |
| CreateFileA. |
| """ |
| |
| import os |
| import subprocess |
| import tempfile |
| |
| |
| class JunctionCreationError(Exception): |
| """Raised when TempJunction fails to create an NTFS junction.""" |
| |
| def __init__(self, path, target, stdout): |
| Exception.__init__( |
| self, |
| "Could not create junction \"%s\" -> \"%s\"\nError from mklink:\n%s" % |
| (path, target, stdout)) |
| |
| |
| class TempJunction(object): |
| r"""Junction in a temp directory. |
| |
| This object creates a temp directory and a junction under it. The junction |
| points to a user-specified path. |
| |
| Use this object if you want to write files under long paths (absolute path at |
| least MAX_PATH (260) chars long). Pass the directory you want to "shorten" as |
| the initializer's argument. This object will create a junction under a shorter |
| path, that points to the long directory. The combined path of the junction and |
| files under it are more likely to be short than the original paths were. |
| |
| Usage example: |
| with TempJunction("C:/some/long/path") as junc: |
| # `junc` created a temp directory and a junction in it that points to |
| # \\?\C:\some\long\path, and is itself shorter than that |
| shortpath = os.path.join(junc, "file.txt") |
| with open(shortpath, "w") as f: |
| ...do something with f... |
| # `junc` deleted the junction and its parent temp directory upon leaving |
| # the `with` statement's body |
| ...do something else... |
| """ |
| |
| def __init__(self, |
| junction_target, |
| testonly_mkdtemp=None, |
| testonly_maxpath=None): |
| """Initialize this object. |
| |
| Args: |
| junction_target: string; an absolute Windows path; the __enter__ method |
| creates a junction that points to this path |
| testonly_mkdtemp: function(); for testing only; a custom function that |
| returns a temp directory path, you can use it to mock out |
| tempfile.mkdtemp |
| testonly_maxpath: int; for testing oly; maximum path length before the |
| path is a "long path" (typically MAX_PATH on Windows) |
| """ |
| self._target = os.path.abspath(junction_target) |
| self._junction = None |
| self._mkdtemp = testonly_mkdtemp or tempfile.mkdtemp |
| self._max_path = testonly_maxpath or 248 |
| |
| @staticmethod |
| def _Mklink(name, target): |
| proc = subprocess.Popen( |
| "cmd.exe /C mklink /J \"%s\" \"\\\\?\\%s\"" % (name, target), |
| stdout=subprocess.PIPE, |
| stderr=subprocess.STDOUT) |
| exitcode = proc.wait() |
| if exitcode != 0: |
| stdout = proc.communicate()[0] |
| raise JunctionCreationError(name, target, stdout) |
| |
| @staticmethod |
| def _TryMkdir(path): |
| try: |
| os.mkdir(path) |
| except OSError as e: |
| # Another process may have already created this directory. |
| if not os.path.isdir(path): |
| raise IOError("Could not create directory at '%s': %s" % (path, str(e))) |
| |
| @staticmethod |
| def _MakeLinks(target, mkdtemp, max_path): |
| """Creates a temp directory and a junction in it, pointing to `target`. |
| |
| Creates all parent directories of `target` if they don't exist. |
| |
| Args: |
| target: string; path to the directory that is the junction's target |
| mkdtemp: function():string; creates a temp directory and returns its |
| absolute path |
| max_path: int; maximum path length before the path is a "long path" |
| (typically MAX_PATH on Windows) |
| Returns: |
| The full path to the junction. |
| Raises: |
| JunctionCreationError: if `mklink` fails to create a junction |
| """ |
| segments = [] |
| dirpath = target |
| while not os.path.isdir(dirpath): |
| dirpath, child = os.path.split(dirpath) |
| if child: |
| segments.append(child) |
| tmp = mkdtemp() |
| juncpath = os.path.join(tmp, "j") |
| for child in reversed(segments): |
| childpath = os.path.join(dirpath, child) |
| if len(childpath) >= max_path: |
| try: |
| TempJunction._Mklink(juncpath, dirpath) |
| TempJunction._TryMkdir(os.path.join(juncpath, child)) |
| finally: |
| os.rmdir(juncpath) |
| else: |
| TempJunction._TryMkdir(childpath) |
| dirpath = childpath |
| TempJunction._Mklink(juncpath, target) |
| return juncpath |
| |
| def __enter__(self): |
| """Creates a temp directory and a junction in it, pointing to self._target. |
| |
| Creates all parent directories of self._target if they don't exist. |
| |
| This method is automatically called upon entering a `with` statement's body. |
| |
| Returns: |
| The full path to the junction. |
| Raises: |
| JunctionCreationError: if `mklink` fails to create a junction |
| """ |
| self._junction = TempJunction._MakeLinks(self._target, self._mkdtemp, |
| self._max_path) |
| return self._junction |
| |
| def __exit__(self, unused_type, unused_value, unused_traceback): |
| """Deletes the junction and its parent directory. |
| |
| This method is automatically called upon leaving a `with` statement's body. |
| |
| Args: |
| unused_type: unused |
| unused_value: unused |
| unused_traceback: unused |
| """ |
| if self._junction: |
| os.rmdir(self._junction) |
| os.rmdir(os.path.dirname(self._junction)) |