1"""
2Virtual environments in the traditional sense are built as reference to the host python. This file allows declarative
3references to elements on the file system, allowing our system to automatically detect what modes it can support given
4the constraints: e.g. can the file system symlink, can the files be read, executed, etc.
5"""
6from __future__ import absolute_import, unicode_literals
7
8import os
9from abc import ABCMeta, abstractmethod
10from collections import OrderedDict
11from stat import S_IXGRP, S_IXOTH, S_IXUSR
12
13from six import add_metaclass
14
15from virtualenv.info import fs_is_case_sensitive, fs_supports_symlink
16from virtualenv.util.path import copy, make_exe, symlink
17from virtualenv.util.six import ensure_text
18
19
20class RefMust(object):
21    NA = "NA"
22    COPY = "copy"
23    SYMLINK = "symlink"
24
25
26class RefWhen(object):
27    ANY = "ANY"
28    COPY = "copy"
29    SYMLINK = "symlink"
30
31
32@add_metaclass(ABCMeta)
33class PathRef(object):
34    """Base class that checks if a file reference can be symlink/copied"""
35
36    FS_SUPPORTS_SYMLINK = fs_supports_symlink()
37    FS_CASE_SENSITIVE = fs_is_case_sensitive()
38
39    def __init__(self, src, must=RefMust.NA, when=RefWhen.ANY):
40        self.must = must
41        self.when = when
42        self.src = src
43        try:
44            self.exists = src.exists()
45        except OSError:
46            self.exists = False
47        self._can_read = None if self.exists else False
48        self._can_copy = None if self.exists else False
49        self._can_symlink = None if self.exists else False
50
51    def __repr__(self):
52        return "{}(src={})".format(self.__class__.__name__, self.src)
53
54    @property
55    def can_read(self):
56        if self._can_read is None:
57            if self.src.is_file():
58                try:
59                    with self.src.open("rb"):
60                        self._can_read = True
61                except OSError:
62                    self._can_read = False
63            else:
64                self._can_read = os.access(ensure_text(str(self.src)), os.R_OK)
65        return self._can_read
66
67    @property
68    def can_copy(self):
69        if self._can_copy is None:
70            if self.must == RefMust.SYMLINK:
71                self._can_copy = self.can_symlink
72            else:
73                self._can_copy = self.can_read
74        return self._can_copy
75
76    @property
77    def can_symlink(self):
78        if self._can_symlink is None:
79            if self.must == RefMust.COPY:
80                self._can_symlink = self.can_copy
81            else:
82                self._can_symlink = self.FS_SUPPORTS_SYMLINK and self.can_read
83        return self._can_symlink
84
85    @abstractmethod
86    def run(self, creator, symlinks):
87        raise NotImplementedError
88
89    def method(self, symlinks):
90        if self.must == RefMust.SYMLINK:
91            return symlink
92        if self.must == RefMust.COPY:
93            return copy
94        return symlink if symlinks else copy
95
96
97@add_metaclass(ABCMeta)
98class ExePathRef(PathRef):
99    """Base class that checks if a executable can be references via symlink/copy"""
100
101    def __init__(self, src, must=RefMust.NA, when=RefWhen.ANY):
102        super(ExePathRef, self).__init__(src, must, when)
103        self._can_run = None
104
105    @property
106    def can_symlink(self):
107        if self.FS_SUPPORTS_SYMLINK:
108            return self.can_run
109        return False
110
111    @property
112    def can_run(self):
113        if self._can_run is None:
114            mode = self.src.stat().st_mode
115            for key in [S_IXUSR, S_IXGRP, S_IXOTH]:
116                if mode & key:
117                    self._can_run = True
118                break
119            else:
120                self._can_run = False
121        return self._can_run
122
123
124class PathRefToDest(PathRef):
125    """Link a path on the file system"""
126
127    def __init__(self, src, dest, must=RefMust.NA, when=RefWhen.ANY):
128        super(PathRefToDest, self).__init__(src, must, when)
129        self.dest = dest
130
131    def run(self, creator, symlinks):
132        dest = self.dest(creator, self.src)
133        method = self.method(symlinks)
134        dest_iterable = dest if isinstance(dest, list) else (dest,)
135        if not dest.parent.exists():
136            dest.parent.mkdir(parents=True, exist_ok=True)
137        for dst in dest_iterable:
138            method(self.src, dst)
139
140
141class ExePathRefToDest(PathRefToDest, ExePathRef):
142    """Link a exe path on the file system"""
143
144    def __init__(self, src, targets, dest, must=RefMust.NA, when=RefWhen.ANY):
145        ExePathRef.__init__(self, src, must, when)
146        PathRefToDest.__init__(self, src, dest, must, when)
147        if not self.FS_CASE_SENSITIVE:
148            targets = list(OrderedDict((i.lower(), None) for i in targets).keys())
149        self.base = targets[0]
150        self.aliases = targets[1:]
151        self.dest = dest
152
153    def run(self, creator, symlinks):
154        bin_dir = self.dest(creator, self.src).parent
155        dest = bin_dir / self.base
156        method = self.method(symlinks)
157        method(self.src, dest)
158        if not symlinks:
159            make_exe(dest)
160        for extra in self.aliases:
161            link_file = bin_dir / extra
162            if link_file.exists():
163                link_file.unlink()
164            if symlinks:
165                link_file.symlink_to(self.base)
166            else:
167                copy(self.src, link_file)
168            if not symlinks:
169                make_exe(link_file)
170
171    def __repr__(self):
172        return "{}(src={}, alias={})".format(self.__class__.__name__, self.src, self.aliases)
173