From d5cec5e752821ca2710101b626b3a3ca07fdb7f8 Mon Sep 17 00:00:00 2001 From: Renaud Paquay Date: Tue, 1 Nov 2016 11:24:03 -0700 Subject: [PATCH] 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 --- manifest_xml.py | 3 +- platform_utils.py | 43 ++++++++++++++++++++++++++++ platform_utils_win32.py | 63 +++++++++++++++++++++++++++++++++++++++++ project.py | 9 ++++-- 4 files changed, 114 insertions(+), 4 deletions(-) create mode 100644 platform_utils_win32.py 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: import gitc_utils from git_config import GitConfig from git_refs import R_HEADS, HEAD +import platform_utils from project import RemoteSpec, Project, MetaProject from error import ManifestParseError, ManifestInvalidRevisionError @@ -166,7 +167,7 @@ class XmlManifest(object): try: if os.path.lexists(self.manifestFile): os.remove(self.manifestFile) - os.symlink(os.path.join('manifests', name), self.manifestFile) + platform_utils.symlink(os.path.join('manifests', name), self.manifestFile) except OSError as e: raise ManifestParseError('cannot link manifest %s: %s' % (name, str(e))) 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): self.queue.put(_FileDescriptorStreamsThreads.QueueItem(self, line)) self.fd.close() self.queue.put(_FileDescriptorStreamsThreads.QueueItem(self, None)) + + +def symlink(source, link_name): + """Creates a symbolic link pointing to source named link_name. + Note: On Windows, source must exist on disk, as the implementation needs + to know whether to create a "File" or a "Directory" symbolic link. + """ + if isWindows(): + import platform_utils_win32 + source = _validate_winpath(source) + link_name = _validate_winpath(link_name) + target = os.path.join(os.path.dirname(link_name), source) + if os.path.isdir(target): + platform_utils_win32.create_dirsymlink(source, link_name) + else: + platform_utils_win32.create_filesymlink(source, link_name) + else: + return os.symlink(source, link_name) + + +def _validate_winpath(path): + path = os.path.normpath(path) + if _winpath_is_valid(path): + return path + raise ValueError("Path \"%s\" must be a relative path or an absolute " + "path starting with a drive letter".format(path)) + + +def _winpath_is_valid(path): + """Windows only: returns True if path is relative (e.g. ".\\foo") or is + absolute including a drive letter (e.g. "c:\\foo"). Returns False if path + is ambiguous (e.g. "x:foo" or "\\foo"). + """ + assert isWindows() + path = os.path.normpath(path) + drive, tail = os.path.splitdrive(path) + if tail: + if not drive: + return tail[0] != os.sep # "\\foo" is invalid + else: + return tail[0] == os.sep # "x:foo" is invalid + else: + 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 @@ +# +# Copyright (C) 2016 The Android Open Source Project +# +# 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. + +import errno + +from ctypes import WinDLL, get_last_error, FormatError, WinError +from ctypes.wintypes import BOOL, LPCWSTR, DWORD + +kernel32 = WinDLL('kernel32', use_last_error=True) + +# Win32 error codes +ERROR_SUCCESS = 0 +ERROR_PRIVILEGE_NOT_HELD = 1314 + +# Win32 API entry points +CreateSymbolicLinkW = kernel32.CreateSymbolicLinkW +CreateSymbolicLinkW.restype = BOOL +CreateSymbolicLinkW.argtypes = (LPCWSTR, # lpSymlinkFileName In + LPCWSTR, # lpTargetFileName In + DWORD) # dwFlags In + +# Symbolic link creation flags +SYMBOLIC_LINK_FLAG_FILE = 0x00 +SYMBOLIC_LINK_FLAG_DIRECTORY = 0x01 + + +def create_filesymlink(source, link_name): + """Creates a Windows file symbolic link source pointing to link_name.""" + _create_symlink(source, link_name, SYMBOLIC_LINK_FLAG_FILE) + + +def create_dirsymlink(source, link_name): + """Creates a Windows directory symbolic link source pointing to link_name. + """ + _create_symlink(source, link_name, SYMBOLIC_LINK_FLAG_DIRECTORY) + + +def _create_symlink(source, link_name, dwFlags): + # Note: Win32 documentation for CreateSymbolicLink is incorrect. + # On success, the function returns "1". + # On error, the function returns some random value (e.g. 1280). + # The best bet seems to use "GetLastError" and check for error/success. + CreateSymbolicLinkW(link_name, source, dwFlags) + code = get_last_error() + if code != ERROR_SUCCESS: + error_desc = FormatError(code).strip() + if code == ERROR_PRIVILEGE_NOT_HELD: + raise OSError(errno.EPERM, error_desc, link_name) + error_desc = 'Error creating symbolic link %s: %s'.format( + link_name, error_desc) + raise WinError(code, error_desc) diff --git a/project.py b/project.py index 269fd7e5..de5c7915 100644 --- a/project.py +++ b/project.py @@ -35,6 +35,7 @@ from git_config import GitConfig, IsId, GetSchemeFromUrl, GetUrlCookieFile, \ from error import GitError, HookError, UploadError, DownloadError from error import ManifestInvalidRevisionError from error import NoManifestException +import platform_utils from trace import IsTrace, Trace from git_refs import GitRefs, HEAD, R_HEADS, R_TAGS, R_PUB, R_M @@ -277,7 +278,7 @@ class _LinkFile(object): dest_dir = os.path.dirname(absDest) if not os.path.isdir(dest_dir): os.makedirs(dest_dir) - os.symlink(relSrc, absDest) + platform_utils.symlink(relSrc, absDest) except IOError: _error('Cannot link file %s to %s', relSrc, absDest) @@ -2379,7 +2380,8 @@ class Project(object): self.relpath, name) continue try: - os.symlink(os.path.relpath(stock_hook, os.path.dirname(dst)), dst) + platform_utils.symlink( + os.path.relpath(stock_hook, os.path.dirname(dst)), dst) except OSError as e: if e.errno == errno.EPERM: raise GitError('filesystem must support symlinks') @@ -2478,7 +2480,8 @@ class Project(object): os.makedirs(src) if name in to_symlink: - os.symlink(os.path.relpath(src, os.path.dirname(dst)), dst) + platform_utils.symlink( + os.path.relpath(src, os.path.dirname(dst)), dst) elif copy_all and not os.path.islink(dst): if os.path.isdir(src): shutil.copytree(src, dst)