blob: 97fa8d29f0654b89c34b7ca05c143cf0ae28cadc [file] [log] [blame]
# 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))