blob: 97fa8d29f0654b89c34b7ca05c143cf0ae28cadc [file] [log] [blame]
Laszlo Csomorc77f8912017-09-05 09:35:39 +02001# 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
17Only use this class on Windows, do not use on other platforms. Other platforms
18support longer paths than Windows, and also support symlinks. Windows only
19supports junctions (directory symlinks).
20
21Junctions are useful if you need to shorten a long path. A long path is one that
22is 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
24CreateFileA.
25"""
26
27import os
28import subprocess
29import tempfile
30
31
32class 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
42class 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 Csomorec91d202017-09-27 06:17:44 -040066 def __init__(self,
67 junction_target,
68 testonly_mkdtemp=None,
69 testonly_maxpath=None):
Laszlo Csomorc77f8912017-09-05 09:35:39 +020070 """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 Csomorec91d202017-09-27 06:17:44 -040078 testonly_maxpath: int; for testing oly; maximum path length before the
79 path is a "long path" (typically MAX_PATH on Windows)
Laszlo Csomorc77f8912017-09-05 09:35:39 +020080 """
Laszlo Csomorec91d202017-09-27 06:17:44 -040081 self._target = os.path.abspath(junction_target)
Laszlo Csomorc77f8912017-09-05 09:35:39 +020082 self._junction = None
83 self._mkdtemp = testonly_mkdtemp or tempfile.mkdtemp
Laszlo Csomorec91d202017-09-27 06:17:44 -040084 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 Csomorc77f8912017-09-05 09:35:39 +0200144
145 def __enter__(self):
146 """Creates a temp directory and a junction in it, pointing to self._target.
147
Laszlo Csomorec91d202017-09-27 06:17:44 -0400148 Creates all parent directories of self._target if they don't exist.
149
Laszlo Csomorc77f8912017-09-05 09:35:39 +0200150 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 Csomorec91d202017-09-27 06:17:44 -0400157 self._junction = TempJunction._MakeLinks(self._target, self._mkdtemp,
158 self._max_path)
159 return self._junction
Laszlo Csomorc77f8912017-09-05 09:35:39 +0200160
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))