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
This commit is contained in:
Renaud Paquay 2016-11-01 11:24:03 -07:00 committed by David Pursehouse
parent 2e70291162
commit d5cec5e752
4 changed files with 114 additions and 4 deletions

View File

@ -32,6 +32,7 @@ else:
import gitc_utils import gitc_utils
from git_config import GitConfig from git_config import GitConfig
from git_refs import R_HEADS, HEAD from git_refs import R_HEADS, HEAD
import platform_utils
from project import RemoteSpec, Project, MetaProject from project import RemoteSpec, Project, MetaProject
from error import ManifestParseError, ManifestInvalidRevisionError from error import ManifestParseError, ManifestInvalidRevisionError
@ -166,7 +167,7 @@ class XmlManifest(object):
try: try:
if os.path.lexists(self.manifestFile): if os.path.lexists(self.manifestFile):
os.remove(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: except OSError as e:
raise ManifestParseError('cannot link manifest %s: %s' % (name, str(e))) raise ManifestParseError('cannot link manifest %s: %s' % (name, str(e)))

View File

@ -167,3 +167,46 @@ class _FileDescriptorStreamsThreads(FileDescriptorStreams):
self.queue.put(_FileDescriptorStreamsThreads.QueueItem(self, line)) self.queue.put(_FileDescriptorStreamsThreads.QueueItem(self, line))
self.fd.close() self.fd.close()
self.queue.put(_FileDescriptorStreamsThreads.QueueItem(self, None)) 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

63
platform_utils_win32.py Normal file
View File

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

View File

@ -35,6 +35,7 @@ from git_config import GitConfig, IsId, GetSchemeFromUrl, GetUrlCookieFile, \
from error import GitError, HookError, UploadError, DownloadError from error import GitError, HookError, UploadError, DownloadError
from error import ManifestInvalidRevisionError from error import ManifestInvalidRevisionError
from error import NoManifestException from error import NoManifestException
import platform_utils
from trace import IsTrace, Trace from trace import IsTrace, Trace
from git_refs import GitRefs, HEAD, R_HEADS, R_TAGS, R_PUB, R_M 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) dest_dir = os.path.dirname(absDest)
if not os.path.isdir(dest_dir): if not os.path.isdir(dest_dir):
os.makedirs(dest_dir) os.makedirs(dest_dir)
os.symlink(relSrc, absDest) platform_utils.symlink(relSrc, absDest)
except IOError: except IOError:
_error('Cannot link file %s to %s', relSrc, absDest) _error('Cannot link file %s to %s', relSrc, absDest)
@ -2379,7 +2380,8 @@ class Project(object):
self.relpath, name) self.relpath, name)
continue continue
try: 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: except OSError as e:
if e.errno == errno.EPERM: if e.errno == errno.EPERM:
raise GitError('filesystem must support symlinks') raise GitError('filesystem must support symlinks')
@ -2478,7 +2480,8 @@ class Project(object):
os.makedirs(src) os.makedirs(src)
if name in to_symlink: 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): elif copy_all and not os.path.islink(dst):
if os.path.isdir(src): if os.path.isdir(src):
shutil.copytree(src, dst) shutil.copytree(src, dst)