# 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 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))