diff options
| author | Renaud Paquay <rpaquay@google.com> | 2016-11-01 11:24:03 -0700 |
|---|---|---|
| committer | David Pursehouse <dpursehouse@collab.net> | 2017-05-29 19:30:34 +0900 |
| commit | d5cec5e752821ca2710101b626b3a3ca07fdb7f8 (patch) | |
| tree | 4ecee491de2d3d57b4d03f526701c8c06a133b17 | |
| parent | 2e7029116204cf2d6f516e4514091f0b492bc689 (diff) | |
| download | git-repo-d5cec5e752821ca2710101b626b3a3ca07fdb7f8.tar.gz | |
Add support for creating symbolic links on Windows
Replace all calls to os.symlink with platform_utils.symlink.
The Windows implementation calls into the CreateSymbolicLinkW Win32
API, as os.symlink is not supported.
Separate the Win32 API definitions into a separate module
platform_utils_win32 for clarity.
Change-Id: I0714c598664c2df93383734e609d948692c17ec5
| -rw-r--r-- | manifest_xml.py | 3 | ||||
| -rw-r--r-- | platform_utils.py | 43 | ||||
| -rw-r--r-- | platform_utils_win32.py | 63 | ||||
| -rw-r--r-- | project.py | 9 |
4 files changed, 114 insertions, 4 deletions
diff --git a/manifest_xml.py b/manifest_xml.py index 55d25a79..05651c6c 100644 --- a/manifest_xml.py +++ b/manifest_xml.py | |||
| @@ -32,6 +32,7 @@ else: | |||
| 32 | import gitc_utils | 32 | import gitc_utils |
| 33 | from git_config import GitConfig | 33 | from git_config import GitConfig |
| 34 | from git_refs import R_HEADS, HEAD | 34 | from git_refs import R_HEADS, HEAD |
| 35 | import platform_utils | ||
| 35 | from project import RemoteSpec, Project, MetaProject | 36 | from project import RemoteSpec, Project, MetaProject |
| 36 | from error import ManifestParseError, ManifestInvalidRevisionError | 37 | from error import ManifestParseError, ManifestInvalidRevisionError |
| 37 | 38 | ||
| @@ -166,7 +167,7 @@ class XmlManifest(object): | |||
| 166 | try: | 167 | try: |
| 167 | if os.path.lexists(self.manifestFile): | 168 | if os.path.lexists(self.manifestFile): |
| 168 | os.remove(self.manifestFile) | 169 | os.remove(self.manifestFile) |
| 169 | os.symlink(os.path.join('manifests', name), self.manifestFile) | 170 | platform_utils.symlink(os.path.join('manifests', name), self.manifestFile) |
| 170 | except OSError as e: | 171 | except OSError as e: |
| 171 | raise ManifestParseError('cannot link manifest %s: %s' % (name, str(e))) | 172 | raise ManifestParseError('cannot link manifest %s: %s' % (name, str(e))) |
| 172 | 173 | ||
diff --git a/platform_utils.py b/platform_utils.py index 1c719b1d..f4dfa0b1 100644 --- a/platform_utils.py +++ b/platform_utils.py | |||
| @@ -167,3 +167,46 @@ class _FileDescriptorStreamsThreads(FileDescriptorStreams): | |||
| 167 | self.queue.put(_FileDescriptorStreamsThreads.QueueItem(self, line)) | 167 | self.queue.put(_FileDescriptorStreamsThreads.QueueItem(self, line)) |
| 168 | self.fd.close() | 168 | self.fd.close() |
| 169 | self.queue.put(_FileDescriptorStreamsThreads.QueueItem(self, None)) | 169 | self.queue.put(_FileDescriptorStreamsThreads.QueueItem(self, None)) |
| 170 | |||
| 171 | |||
| 172 | def symlink(source, link_name): | ||
| 173 | """Creates a symbolic link pointing to source named link_name. | ||
| 174 | Note: On Windows, source must exist on disk, as the implementation needs | ||
| 175 | to know whether to create a "File" or a "Directory" symbolic link. | ||
| 176 | """ | ||
| 177 | if isWindows(): | ||
| 178 | import platform_utils_win32 | ||
| 179 | source = _validate_winpath(source) | ||
| 180 | link_name = _validate_winpath(link_name) | ||
| 181 | target = os.path.join(os.path.dirname(link_name), source) | ||
| 182 | if os.path.isdir(target): | ||
| 183 | platform_utils_win32.create_dirsymlink(source, link_name) | ||
| 184 | else: | ||
| 185 | platform_utils_win32.create_filesymlink(source, link_name) | ||
| 186 | else: | ||
| 187 | return os.symlink(source, link_name) | ||
| 188 | |||
| 189 | |||
| 190 | def _validate_winpath(path): | ||
| 191 | path = os.path.normpath(path) | ||
| 192 | if _winpath_is_valid(path): | ||
| 193 | return path | ||
| 194 | raise ValueError("Path \"%s\" must be a relative path or an absolute " | ||
| 195 | "path starting with a drive letter".format(path)) | ||
| 196 | |||
| 197 | |||
| 198 | def _winpath_is_valid(path): | ||
| 199 | """Windows only: returns True if path is relative (e.g. ".\\foo") or is | ||
| 200 | absolute including a drive letter (e.g. "c:\\foo"). Returns False if path | ||
| 201 | is ambiguous (e.g. "x:foo" or "\\foo"). | ||
| 202 | """ | ||
| 203 | assert isWindows() | ||
| 204 | path = os.path.normpath(path) | ||
| 205 | drive, tail = os.path.splitdrive(path) | ||
| 206 | if tail: | ||
| 207 | if not drive: | ||
| 208 | return tail[0] != os.sep # "\\foo" is invalid | ||
| 209 | else: | ||
| 210 | return tail[0] == os.sep # "x:foo" is invalid | ||
| 211 | else: | ||
| 212 | return not drive # "x:" is invalid | ||
diff --git a/platform_utils_win32.py b/platform_utils_win32.py new file mode 100644 index 00000000..02fb013a --- /dev/null +++ b/platform_utils_win32.py | |||
| @@ -0,0 +1,63 @@ | |||
| 1 | # | ||
| 2 | # Copyright (C) 2016 The Android Open Source Project | ||
| 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 | |||
| 16 | import errno | ||
| 17 | |||
| 18 | from ctypes import WinDLL, get_last_error, FormatError, WinError | ||
| 19 | from ctypes.wintypes import BOOL, LPCWSTR, DWORD | ||
| 20 | |||
| 21 | kernel32 = WinDLL('kernel32', use_last_error=True) | ||
| 22 | |||
| 23 | # Win32 error codes | ||
| 24 | ERROR_SUCCESS = 0 | ||
| 25 | ERROR_PRIVILEGE_NOT_HELD = 1314 | ||
| 26 | |||
| 27 | # Win32 API entry points | ||
| 28 | CreateSymbolicLinkW = kernel32.CreateSymbolicLinkW | ||
| 29 | CreateSymbolicLinkW.restype = BOOL | ||
| 30 | CreateSymbolicLinkW.argtypes = (LPCWSTR, # lpSymlinkFileName In | ||
| 31 | LPCWSTR, # lpTargetFileName In | ||
| 32 | DWORD) # dwFlags In | ||
| 33 | |||
| 34 | # Symbolic link creation flags | ||
| 35 | SYMBOLIC_LINK_FLAG_FILE = 0x00 | ||
| 36 | SYMBOLIC_LINK_FLAG_DIRECTORY = 0x01 | ||
| 37 | |||
| 38 | |||
| 39 | def create_filesymlink(source, link_name): | ||
| 40 | """Creates a Windows file symbolic link source pointing to link_name.""" | ||
| 41 | _create_symlink(source, link_name, SYMBOLIC_LINK_FLAG_FILE) | ||
| 42 | |||
| 43 | |||
| 44 | def create_dirsymlink(source, link_name): | ||
| 45 | """Creates a Windows directory symbolic link source pointing to link_name. | ||
| 46 | """ | ||
| 47 | _create_symlink(source, link_name, SYMBOLIC_LINK_FLAG_DIRECTORY) | ||
| 48 | |||
| 49 | |||
| 50 | def _create_symlink(source, link_name, dwFlags): | ||
| 51 | # Note: Win32 documentation for CreateSymbolicLink is incorrect. | ||
| 52 | # On success, the function returns "1". | ||
| 53 | # On error, the function returns some random value (e.g. 1280). | ||
| 54 | # The best bet seems to use "GetLastError" and check for error/success. | ||
| 55 | CreateSymbolicLinkW(link_name, source, dwFlags) | ||
| 56 | code = get_last_error() | ||
| 57 | if code != ERROR_SUCCESS: | ||
| 58 | error_desc = FormatError(code).strip() | ||
| 59 | if code == ERROR_PRIVILEGE_NOT_HELD: | ||
| 60 | raise OSError(errno.EPERM, error_desc, link_name) | ||
| 61 | error_desc = 'Error creating symbolic link %s: %s'.format( | ||
| 62 | link_name, error_desc) | ||
| 63 | raise WinError(code, error_desc) | ||
| @@ -35,6 +35,7 @@ from git_config import GitConfig, IsId, GetSchemeFromUrl, GetUrlCookieFile, \ | |||
| 35 | from error import GitError, HookError, UploadError, DownloadError | 35 | from error import GitError, HookError, UploadError, DownloadError |
| 36 | from error import ManifestInvalidRevisionError | 36 | from error import ManifestInvalidRevisionError |
| 37 | from error import NoManifestException | 37 | from error import NoManifestException |
| 38 | import platform_utils | ||
| 38 | from trace import IsTrace, Trace | 39 | from trace import IsTrace, Trace |
| 39 | 40 | ||
| 40 | from git_refs import GitRefs, HEAD, R_HEADS, R_TAGS, R_PUB, R_M | 41 | from git_refs import GitRefs, HEAD, R_HEADS, R_TAGS, R_PUB, R_M |
| @@ -277,7 +278,7 @@ class _LinkFile(object): | |||
| 277 | dest_dir = os.path.dirname(absDest) | 278 | dest_dir = os.path.dirname(absDest) |
| 278 | if not os.path.isdir(dest_dir): | 279 | if not os.path.isdir(dest_dir): |
| 279 | os.makedirs(dest_dir) | 280 | os.makedirs(dest_dir) |
| 280 | os.symlink(relSrc, absDest) | 281 | platform_utils.symlink(relSrc, absDest) |
| 281 | except IOError: | 282 | except IOError: |
| 282 | _error('Cannot link file %s to %s', relSrc, absDest) | 283 | _error('Cannot link file %s to %s', relSrc, absDest) |
| 283 | 284 | ||
| @@ -2379,7 +2380,8 @@ class Project(object): | |||
| 2379 | self.relpath, name) | 2380 | self.relpath, name) |
| 2380 | continue | 2381 | continue |
| 2381 | try: | 2382 | try: |
| 2382 | os.symlink(os.path.relpath(stock_hook, os.path.dirname(dst)), dst) | 2383 | platform_utils.symlink( |
| 2384 | os.path.relpath(stock_hook, os.path.dirname(dst)), dst) | ||
| 2383 | except OSError as e: | 2385 | except OSError as e: |
| 2384 | if e.errno == errno.EPERM: | 2386 | if e.errno == errno.EPERM: |
| 2385 | raise GitError('filesystem must support symlinks') | 2387 | raise GitError('filesystem must support symlinks') |
| @@ -2478,7 +2480,8 @@ class Project(object): | |||
| 2478 | os.makedirs(src) | 2480 | os.makedirs(src) |
| 2479 | 2481 | ||
| 2480 | if name in to_symlink: | 2482 | if name in to_symlink: |
| 2481 | os.symlink(os.path.relpath(src, os.path.dirname(dst)), dst) | 2483 | platform_utils.symlink( |
| 2484 | os.path.relpath(src, os.path.dirname(dst)), dst) | ||
| 2482 | elif copy_all and not os.path.islink(dst): | 2485 | elif copy_all and not os.path.islink(dst): |
| 2483 | if os.path.isdir(src): | 2486 | if os.path.isdir(src): |
| 2484 | shutil.copytree(src, dst) | 2487 | shutil.copytree(src, dst) |
