-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathconanfile.py
1218 lines (1084 loc) · 46.2 KB
/
conanfile.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
import itertools
import operator
import os
import pathlib
import re
import struct
import subprocess
import sys
import textwrap
import time
from contextlib import contextmanager
from io import BytesIO
from pathlib import Path
from zipfile import ZipFile, ZipInfo
from conan import ConanFile, conan_version
from conan.tools.env import Environment
from conan.tools.files import replace_in_file
from conan.tools.scm import Version
from pip._vendor.distlib.resources import finder
from pip._vendor.distlib.util import FileOperator, get_platform
class PythonVirtualEnvironmentPackage(ConanFile):
name = "pyvenv"
version = "0.2.2"
url = "https://github.com/samuel-emrys/pyvenv.git"
homepage = "https://github.com/samuel-emrys/pyvenv.git"
license = "MIT"
description = (
"A python_requires library providing a management class for python "
"virtual environments and a CMake generator to expose executables in "
"those virtual environments as CMake targets"
)
topics = ("Python", "Virtual Environment", "CMake", "venv")
no_copy_source = True
package_type = "python-require"
def args_to_string(args):
"""
Convert a list of arguments to a command line string in an operating
system agnostic way
:param args: A list of the arguments to provide to the command line
:type args: list(str)
"""
if not args:
return ""
if sys.platform == "win32":
return subprocess.list2cmdline(args)
else:
return " ".join("'" + arg.replace("'", r"'\''") + "'" for arg in args)
def _which(files, paths, access=os.F_OK | os.X_OK):
"""
Mostly like shutil.which, but allows searching for alternate filenames,
and never falls back to %PATH% or curdir
"""
if isinstance(files, str):
files = [files]
if sys.platform == "win32":
pathext = os.environ.get("PATHEXT", "").split(os.pathsep)
def expand_pathext(cmd):
if any(cmd.lower().endswith(ext.lower()) for ext in pathext):
yield cmd # already has an extension, so check only that one
else:
# check all possibilities
yield from (cmd + ext for ext in pathext)
files = [x for cmd in files for x in expand_pathext(cmd)]
# Windows filesystems are (usually) case-insensitive, so match might be
# spelled differently than the searched name.
# And in particular, the extensions from PATHEXT are usually uppercase,
# and yet the real file seldom is.
# Using pathlib.resolve() for now because os.path.realpath() was a
# no-op on win32 until nt symlink support landed in python 3.9 (based
# on GetFinalPathNameByHandleW)
# https://github.com/python/cpython/commit/75e064962ee0e31ec19a8081e9d9cc957baf6415
#
# realname() canonicalizes *only* the searched-for filename, but
# keeps the caller-provided path verbatim: they might have been short
# paths, or via some symlink, and that's fine
def realname(file):
path = Path(file)
realname = path.resolve(strict=True).name
return str(path.with_name(realname))
else:
def realname(path):
return path # no-op
for path in paths:
for file in files:
filepath = os.path.join(path, file)
if (
os.path.exists(filepath)
and os.access(filepath, access)
and not os.path.isdir(filepath)
): # is executable
return realname(filepath)
return None
def _default_python():
"""
Identify the default python interpreter.
"""
base_exec_prefix = sys.base_exec_prefix
if hasattr(
sys, "real_prefix"
): # in a venv, which sets this instead of base_exec_prefix like venv
base_exec_prefix = getattr(sys, "real_prefix")
if sys.exec_prefix != base_exec_prefix: # alread running in a venv
# we want to create the new virtualenv off the base python
# installation, rather than create a grandchild (child of of
# the current venv)
names = [os.path.basename(sys.executable), "python3", "python"]
prefixes = [base_exec_prefix]
suffixes = ["bin", "Scripts"]
exec_prefix_suffix = os.path.relpath(
os.path.dirname(sys.executable), sys.exec_prefix
) # e.g. bin or Scripts
if exec_prefix_suffix and exec_prefix_suffix != ".":
suffixes.insert(0, exec_prefix_suffix)
def add_suffix(prefix, suffixes):
yield prefix
yield from (os.path.join(prefix, suffix) for suffix in suffixes)
dirs = [x for prefix in prefixes for x in add_suffix(prefix, suffixes)]
return _which(names, dirs)
else:
return sys.executable
def _write_activate_this(env_dir, bin_dir, lib_dirs):
"""
Write an activate_this.py to env_dir. This fills a gap where this isn't
created by default by the `venv` module. This borrows the implementation
from `virtualenv`.
:param env_dir: The path to the virtual environment directory
:type env_dir: str
:param bin_dir: The name of the platform specific binary directory.
`Scripts` or `bin`.
:type bin_dir: str
:param lib_dirs: A list of the environment library directories to
search when activate_this.py is used
:type lib_dirs: list(str)
"""
win_py2 = sys.platform == "win32" and sys.version_info.major == 2
decode_path = "yes" if win_py2 else ""
lib_dirs = [
os.path.relpath(libdir, os.path.join(env_dir, bin_dir)) for libdir in lib_dirs
]
lib_dirs = os.pathsep.join(lib_dirs)
contents = textwrap.dedent(
f"""\
import os
import site
import sys
try:
abs_file = os.path.abspath(__file__)
except NameError:
raise AssertionError("You must use exec(open(this_file).read(), {{'__file__': this_file}}))")
bin_dir = os.path.dirname(abs_file)
base = bin_dir[: -len("{bin_dir}") - 1] # strip away the bin part from the __file__, plus the path separator
# prepend bin to PATH (this file is inside the bin directory)
os.environ["PATH"] = os.pathsep.join([bin_dir] + os.environ.get("PATH", "").split(os.pathsep))
os.environ["VIRTUAL_ENV"] = base # virtual env is right above bin directory
# add the virtual environments libraries to the host python import mechanism
prev_length = len(sys.path)
for lib in "{lib_dirs}".split(os.pathsep):
path = os.path.realpath(os.path.join(bin_dir, lib))
site.addsitedir(path.decode("utf-8") if "{decode_path}" else path)
sys.path[:] = sys.path[prev_length:] + sys.path[0:prev_length]
sys.real_prefix = sys.prefix
sys.prefix = base
"""
)
with open(os.path.join(env_dir, bin_dir, "activate_this.py"), "w") as f:
f.write(contents)
# build helper for making and managing python virtual environments
class PythonVirtualEnv:
"""
A build helper for creating and managing python virtual environments
"""
def __init__(self, conanfile, interpreter=_default_python(), env_folder=None):
"""
Create a PythonVirtualEnv object
:param conanfile: A reference to the conanfile invoking the PythonVirtualEnv object
:type package: ConanFile
:param interpreter: A path to the interpreter to use for the virtual environment. Defaults
to the first python discovered on the PATH.
:type package: str
:param env_folder: The directory for the python virtual environment to manage or create. Defaults to `None`.
:type package: str
"""
self._conanfile = conanfile
self.base_python = interpreter
self.env_folder = env_folder
self._debug = (
self._conanfile.output.debug
if (Version(conan_version).major >= 2)
else self._conanfile.output.info
)
def create(
self,
folder,
*,
clear=True,
symlinks=(os.name != "nt"),
with_pip=True,
requirements=[],
):
"""
Create a virtual environment
symlink logic borrowed from python -m venv
See venv.main() in /Lib/venv/__init__
:param folder: The directory in which to create the virtual environment
:type folder: str
:param clear: Delete the contents of the environment directory if it already exists, before environment creation
:type clear: str
:param symlinks: Try to use symlinks rather than copies, when symlinks are not the default for the platform.
Defaults to `False for windows, and `True` otherwise.
:type symlinks: str
:param with_pip: Install pip in the virtual environment. Defaults to `True`.
:type with_pip: str
:param requirements: A list of requirements to install in the virtual environment
:type requirements: list(str)
"""
self.env_folder = folder
self._conanfile.output.info(
f"creating venv at {self.env_folder} based on {self.base_python or '<conanfile>'}"
)
if self.base_python:
# another alternative (if we ever wanted to support more customization) would be to launch
# a `python -` subprocess and feed it the script text `import venv venv.EnvBuilder() ...` on stdin
venv_options = ["--symlinks" if symlinks else "--copies"]
if clear:
venv_options.append("--clear")
if not with_pip:
venv_options.append("--without-pip")
env = Environment()
env.define("__PYVENV_LAUNCHER__", None)
envvars = env.vars(self._conanfile, scope="build")
envvars.save_script("build_python")
self._conanfile.run(
args_to_string(
[self.base_python, "-mvenv", *venv_options, self.env_folder]
),
env="conanbuild",
)
else:
# fallback to using the python this script is running in
# (risks the new venv having an inadvertant dependency if conan itself is virtualized somehow, but it will *work*)
import venv
builder = venv.EnvBuilder(clear=clear, symlinks=symlinks, with_pip=with_pip)
builder.create(self.env_folder)
if requirements:
self._conanfile.run(
args_to_string([self.pip, "install", *requirements]), env="conanbuild"
)
_write_activate_this(
env_dir=self.env_folder, bin_dir=self._bin_dir, lib_dirs=self._libpath
)
self.make_relocatable(env_folder=self.env_folder)
def entry_points(self, package=None):
"""
Retrieve the entry points available for a package
:param package: The package to return entry points for. Default is `None`
:type package: str
"""
import importlib.metadata # Python 3.8 or greater
entry_points = itertools.chain.from_iterable(
dist.entry_points
for dist in importlib.metadata.distributions(
name=package, path=self._libpath
)
)
by_group = operator.attrgetter("group")
ordered = sorted(entry_points, key=by_group)
grouped = itertools.groupby(ordered, by_group)
return {
group: [x.name for x in entry_points] for group, entry_points in grouped
}
def setup_entry_points(self, package, folder, silent=False):
"""
Add entry points for a package to the virtual environment. In most cases this shouldn't be required.
:param package: The package to add entry points for
:type package: str
:param folder: The directory in which to configure the entry points.
:type folder: str
:param silent: Suppress log output. Default is `False`
:type silent: bool
"""
# create target folder
try:
os.makedirs(folder)
except Exception:
pass
def copy_executable(name, target_folder, type):
import shutil
# locate script in venv
try:
path = self.which(name, required=True)
self._conanfile.output.info(f"Found {name} at {path}")
except FileNotFoundError as e:
# avoid FileNotFound if the no launcher script for this name was found, or
self._conanfile.output.warning(
f"pyvenv.setup_entry_points: FileNotFoundError: {e}"
)
return
root, ext = os.path.splitext(path)
self._conanfile.output.info(f"{name} split into {root}, {ext}")
try:
# copy venv script to target folder
self._conanfile.output.info(
f"Attempting to copy {path} to {target_folder}"
)
shutil.copy2(path, target_folder)
# copy entry point script
# if it exists
if type == "gui":
ext = "-script.pyw"
else:
ext = "-script.py"
entry_point_script = root + ext
self._conanfile.output.info(
f"Entry point script evaluated to {entry_point_script}"
)
if os.path.isfile(entry_point_script):
self._conanfile.output.info(
f"Attempting to copy {entry_point_script} to {target_folder}"
)
shutil.copy2(entry_point_script, target_folder)
except shutil.SameFileError:
# SameFileError if the launcher script is *already* in the target_folder
# e.g. on posix systems the venv scripts are already in bin/
if not silent:
self._conanfile.output.info(
f"pyvenv.setup_entry_points: command '{name}' already found in '{folder}'. Other entry_points may also be unintentionally visible."
)
entry_points = self.entry_points(package)
for name in entry_points.get("console_scripts", []):
self._conanfile.output.info(f"Adding entry point for {name}")
copy_executable(name, folder, type="console")
for name in entry_points.get("gui_scripts", []):
self._conanfile.output.info(f"Adding entry point for {name}")
copy_executable(name, folder, type="gui")
@property
def _version(self):
return "{}.{}".format(*sys.version_info)
@property
def _python_version(self):
return f"python{self._version}"
@property
def _is_pypy(self):
return hasattr(sys, "pypy_version_info")
@property
def _is_win(self):
return sys.platform == "win32"
@property
def _abi_flags(self):
return getattr(sys, "abiflags", "")
@property
def _bin_dir(self):
return "Scripts" if self._is_win else "bin"
@property
def _binpath(self):
# this should be the same logic as as
# context.bin_name = ... in venv.ensure_directories
bindirs = [self._bin_dir]
return [os.path.join(self.env_folder, x) for x in bindirs]
@property
def _libpath(self):
# this should be the same logic as as
# libpath = ... in venv.ensure_directories
if self._is_win:
libpath = os.path.join(self.env_folder, "Lib", "site-packages")
else:
libpath = os.path.join(
self.env_folder,
"lib",
"python%d.%d" % sys.version_info[:2],
"site-packages",
)
return [libpath]
# return the path to a command within the venv, None if only found outside
def which(self, command, required=False, **kwargs):
found = _which(command, self._binpath, **kwargs)
if found:
return found
elif required:
raise FileNotFoundError(
f"command {command} not in venv binpath {os.pathsep.join(self._binpath)}"
)
else:
return None
@property
def python(self):
"""
Convenience wrapper for python. Can be used to avoid activating the environment when
installing dependencies, i.e.:
self.run(args_to_string([venv.python, "-mpip", "install", "sphinx"])
"""
return self.which("python", required=True)
@property
def pip(self):
"""
Convenience wrapper for pip. Ensure that this is used in conjunction with activate
to ensure that the python interpreter inside the venv is used, i.e.
with venv.activate():
self.run(args_to_string([venv.pip, "install", "sphinx"])
"""
return self.which("pip", required=True)
@property
def _env(self):
"""
environment variables like the usual venv `activate` script, i.e.
with tools.environment_append(venv.env):
...
"""
return {
"__PYVENV_LAUNCHER__": None, # this might already be set if conan was launched through a venv
"PYTHONHOME": None,
"VIRTUAL_ENV": self.env_folder,
"PATH": self._binpath,
}
@contextmanager
def activate(self):
"""
Setup environment and add site_packages of this this venv to sys.path
(importing from the venv only works if it contains python modules compatible
with conan's python interrpreter as well as the venv one
But they're generally the same per _default_python(), so this will let you try
with venv.activate():
...
"""
old_path = sys.path[:]
sys.path.extend(self._libpath)
env = Environment()
for k, v in self._env.items():
env.define(k, v)
envvars = env.vars(self._conanfile, scope="build")
with envvars.apply():
yield
sys.path = old_path
def make_relocatable(self, env_folder):
"""
Makes the already-existing environment use relative paths, and takes out
the #!-based environment selection in scripts.
This functionality does not make a virtual environment relocatable to any
environment other than the one in which it was created in. This is only suitable
for moving a venv directory to another directory in the same environment, such
as would be achieved with `mv venv venv2`.
This functionality is _NOT_ suitable for transplanting a venv for usage in a
deployment environment, where it is on a different physical machine, with a
different python interpreter, or different underlying library requirements, such
as GLIBC. It is only suitable for usage within the same environment in which it
was built.
In a conan context, the resulting virtualenv should not be uploaded to a server.
The `build_policy="missing"` and `upload_policy="skip"` attributes should be set.
Derived from https://github.com/pypa/virtualenv/blob/fb6e546cc1dfd0d363dc4d769486805d2d8f04bc/virtualenv.py#L1890-L1903
MIT License
:param env_folder: The path to the virtual environment to make relocatable
:type env_folder: str
"""
env_dir, lib_dir, inc_dir, bin_dir = self._path_locations(env_folder)
activate_this = os.path.join(bin_dir, "activate_this.py")
if not os.path.exists(activate_this):
self._conanfile.output.error(
f"The environment doesn't have a file {activate_this} -- please re-run virtualenv "
"on this environment to update it"
)
patcher = ScriptPatcher(env_dir, bin_dir, lib_dir, inc_dir, self._conanfile)
patcher.patch_scripts()
patcher.patch_resources()
def _path_locations(self, home_dir):
"""
Return the path locations for the environment (where libraries are,
where scripts go, etc)
Derived from https://github.com/pypa/virtualenv/blob/fb6e546cc1dfd0d363dc4d769486805d2d8f04bc/virtualenv.py#L1199-L1241
MIT License
:param home_dir: The base path to the virtual environment
:type home_dir: str
"""
home_dir = pathlib.Path(home_dir).absolute()
lib_dir, inc_dir, bin_dir = None, None, None
# XXX: We'd use distutils.sysconfig.get_python_inc/lib but its
# prefix arg is broken: http://bugs.python.org/issue3386
if self._is_win:
# Windows has lots of problems with executables with spaces in
# the name; this function will remove them (using the ~1
# format):
home_dir.mkdir(parents=True, exist_ok=True)
if " " in str(home_dir):
import ctypes
get_short_path_name = ctypes.windll.kernel32.GetShortPathNameW
size = max(len(str(home_dir)) + 1, 256)
buf = ctypes.create_unicode_buffer(size)
try:
# noinspection PyUnresolvedReferences
u = unicode
except NameError:
u = str
ret = get_short_path_name(u(home_dir), buf, size)
if not ret:
print('Error: the path "{}" has a space in it'.format(home_dir))
print("We could not determine the short pathname for it.")
print("Exiting.")
sys.exit(3)
home_dir = str(buf.value)
lib_dir = os.path.join(home_dir, "Lib")
inc_dir = os.path.join(home_dir, "Include")
bin_dir = os.path.join(home_dir, "Scripts")
if self._is_pypy:
lib_dir = home_dir
inc_dir = os.path.join(home_dir, "include")
bin_dir = os.path.join(home_dir, "bin")
elif not self._is_win:
lib_dir = os.path.join(home_dir, "lib", self._python_version)
inc_dir = os.path.join(
home_dir, "include", self._python_version + self._abi_flags
)
bin_dir = os.path.join(home_dir, "bin")
return home_dir, lib_dir, inc_dir, bin_dir
class ScriptPatcher:
"""
Class to manage patching files within a virtual environment to make them relocatable.
This is achieved mostly through the replacement of a virtual environment-specific shebang
with one that invokes the system python interpreter, and the insertion of a script to
invoke activate_this.py.
On windows, this will read the contents of scripts wrapped into executables with a python
launcher, and then write a newly wrapped script out.
This has been derived from distlib's ScriptMaker:
https://github.com/pypa/distlib/blob/05375908c1b2d6b0e74bdeb574569d3609db9f56/distlib/scripts.py#L71-L438
"""
def __init__(
self,
env_dir,
bin_dir,
lib_dir,
inc_dir,
conanfile,
add_launchers=True,
fileop=None,
):
self.env_dir = env_dir
self.bin_dir = bin_dir
self.lib_dir = lib_dir
self.inc_dir = inc_dir
self.add_launchers = add_launchers
self.clobber = True
self.set_mode = (os.name == "posix") or (
os.name == "java" and os._name == "posix"
)
self._fileop = fileop or FileOperator()
self._is_nt = os.name == "nt" or (os.name == "java" and os._name == "nt")
self._conanfile = conanfile
self._debug = (
self._conanfile.output.debug
if (Version(conan_version).major >= 2)
else self._conanfile.output.info
)
@property
def _version(self):
return "{}.{}".format(*sys.version_info)
def _read_contents(self, file):
# Read in the contents of the script
if ".exe" in file and self._is_nt:
# Fortunately the exe's can be read as zip files
zip_contents = ZipFile(file)
with zip_contents.open("__main__.py") as zf:
contents = zf.read().decode("utf-8")
else:
with open(file) as f:
contents = f.read()
return contents
def _build_shebang(
self, executable, interpreter, executable_args=[], interpreter_args=[]
):
executable_args = "" if not executable_args else f" {' '.join(executable_args)}"
interpreter_args = (
"" if not interpreter_args else f" {' '.join(interpreter_args)}"
)
return f"#!{executable}{executable_args} {interpreter}{interpreter_args}\n"
def _make_shebang(self):
if self._is_nt:
executable = os.path.normcase(os.environ.get("COMSPEC", "cmd.exe"))
executable_args = ["/c"]
interpreter = "python.exe"
interpreter_args = []
else:
executable = "/usr/bin/env"
executable_args = []
interpreter = f"python{self._version}"
interpreter_args = []
return self._build_shebang(
executable, interpreter, executable_args, interpreter_args
)
def _remove_shebang(self, contents):
shebang_pattern = re.compile(r"^#!.*$")
contents = "\n".join(
[line for line in contents.splitlines() if not shebang_pattern.match(line)]
)
return contents
def _patch_contents(self, contents):
"""
Patch the contents of a file by adding an activation script to invoke activate_this.py in the same directory
:param contents: The file contents to be patched
:type contents: str
:return: The patched contents
:rtype: str
"""
# file needs to account for the fact that __file__ is within a zip file
# on windows but not on *nix
activate = (
"import os; "
"import sys; "
"file=os.path.dirname(os.path.realpath(__file__)) if sys.platform=='win32' else os.path.realpath(__file__); "
"activate_this=os.path.join(os.path.dirname(file), 'activate_this.py'); "
"exec(compile(open(activate_this).read(), activate_this, 'exec'), { '__file__': activate_this}); "
"del os, sys, file, activate_this"
)
contents = activate + "\n" + contents
return contents
def _write_script(self, name, shebang, script_bytes, ext=None):
# We only want to patch a file with a launcher if it's already using a
# launcher (i.e., a .exe file. Leave .py files alone)
use_launcher = self.add_launchers and self._is_nt and name.endswith(".exe")
linesep = os.linesep.encode("utf-8")
if not shebang.endswith(linesep):
shebang += linesep
if not use_launcher:
script_bytes = shebang + script_bytes
else: # pragma: no cover
if ext == "py":
launcher = self._get_launcher("t")
else:
launcher = self._get_launcher("w")
stream = BytesIO()
with ZipFile(stream, "w") as zf:
source_date_epoch = os.environ.get("SOURCE_DATE_EPOCH")
if source_date_epoch:
date_time = time.gmtime(int(source_date_epoch))[:6]
zinfo = ZipInfo(filename="__main__.py", date_time=date_time)
zf.writestr(zinfo, script_bytes)
else:
zf.writestr("__main__.py", script_bytes)
zip_data = stream.getvalue()
script_bytes = launcher + shebang + zip_data
outname = os.path.join(self.bin_dir, name)
if use_launcher: # pragma: no cover
n, e = os.path.splitext(outname)
if e.startswith(".py") or e.startswith(".exe"):
outname = n
outname = "%s.exe" % outname
try:
self._fileop.write_binary_file(outname, script_bytes)
except Exception:
# Failed writing an executable - it might be in use.
self._conanfile.output.warning(
"Failed to write executable - trying to " "use .deleteme logic"
)
dfname = "%s.deleteme" % outname
if os.path.exists(dfname):
os.remove(dfname) # Not allowed to fail here
os.rename(outname, dfname) # nor here
self._fileop.write_binary_file(outname, script_bytes)
self._conanfile.output.debug(
"Able to replace executable using " ".deleteme logic"
)
try:
os.remove(dfname)
except Exception:
pass # still in use - ignore error
else:
if self._is_nt and not outname.endswith("." + ext): # pragma: no cover
outname = "%s.%s" % (outname, ext)
self._conanfile.output.info(f"Renaming {outname=}")
if os.path.exists(outname) and not self.clobber:
self._conanfile.output.warning("Skipping existing file %s", outname)
return
self._fileop.write_binary_file(outname, script_bytes)
if self.set_mode:
self._fileop.set_executable_mode([outname])
if os.name == "nt" or (os.name == "java" and os._name == "nt"): # pragma: no cover
# Executable launcher support.
# Launchers are from https://bitbucket.org/vinay.sajip/simple_launcher/
# This will extract the launcher binary from the distlib module
def _get_launcher(self, kind):
if struct.calcsize("P") == 8: # 64-bit
bits = "64"
else:
bits = "32"
platform_suffix = "-arm" if get_platform() == "win-arm64" else ""
name = "%s%s%s.exe" % (kind, bits, platform_suffix)
# Use distlib from pip
# Issue 31 in distlib repo isn't a concern, we don't need dynamic
# discovery
distlib_package = "pip._vendor.distlib"
resource = finder(distlib_package).find(name)
if not resource:
msg = "Unable to find resource %s in package %s" % (
name,
distlib_package,
)
raise ValueError(msg)
return resource.bytes
def _patch_program_script(self, filename):
contents = self._read_contents(filename)
if "activate_this" in contents:
self._conanfile.output.warning(f"{filename} has already been patched")
return
contents = self._remove_shebang(contents)
shebang = self._make_shebang().encode("utf-8")
script = self._patch_contents(contents).encode("utf-8")
ext = "py"
self._write_script(filename, shebang, script, ext)
@property
def _version(self):
return "{}.{}".format(*sys.version_info)
@property
def _python_version(self):
return f"python{self._version}"
@property
def _dont_patch(self):
return [
"python",
"python3",
self._python_version,
"python.exe",
"python3.exe",
"pythonw.exe",
"activate_this.py",
]
@property
def _activation_scripts(self):
return [
"activate",
"activate.sh",
"activate.bat",
"activate.fish",
"activate.csh",
"activate.xsh",
"activate.nu",
"Activate.ps1",
"deactivate.bat",
]
def _patch_pth_and_egg_link(self, home_dir, sys_path=None):
"""
Makes .pth and .egg-link files use relative paths
Derived from https://github.com/pypa/virtualenv/blob/fb6e546cc1dfd0d363dc4d769486805d2d8f04bc/virtualenv.py#L1990-L2015
MIT License
:param home_dir: The base path to the virtual environment
:type home_dir: str
:param sys_path: The system path to use
:type sys_path: str
"""
home_dir = os.path.normcase(os.path.abspath(home_dir))
if sys_path is None:
sys_path = sys.path
for a_path in sys_path:
if not a_path:
a_path = "."
if not os.path.isdir(a_path):
continue
a_path = os.path.normcase(os.path.abspath(a_path))
if not a_path.startswith(home_dir):
self._debug(f"Skipping system (non-environment) directory {a_path}")
continue
for filename in os.listdir(a_path):
filename = os.path.join(a_path, filename)
if filename.endswith(".pth"):
if not os.access(filename, os.W_OK):
self._conanfile.output.warning(
f"Cannot write .pth file {filename}, skipping"
)
else:
self._patch_pth_file(filename)
if filename.endswith(".egg-link"):
if not os.access(filename, os.W_OK):
self._conanfile.output.warning(
f"Cannot write .egg-link file {filename}, skipping"
)
else:
self._patch_egg_link(filename)
def _patch_pth_file(self, filename):
"""
Derived from https://github.com/pypa/virtualenv/blob/fb6e546cc1dfd0d363dc4d769486805d2d8f04bc/virtualenv.py#L2018-L2036
MIT License
:param filename: Path to the file to fix
:type filename: str
"""
lines = []
with open(filename) as f:
prev_lines = f.readlines()
for line in prev_lines:
line = line.strip()
if (
not line
or line.startswith("#")
or line.startswith("import ")
or os.path.abspath(line) != line
):
lines.append(line)
else:
new_value = self._make_relative_path(filename, line)
if line != new_value:
self._debug(
"Rewriting path {} as {} (in {})".format(
line, new_value, filename
)
)
lines.append(new_value)
if lines == prev_lines:
self._conanfile.output.info(f"No changes to .pth file {filename}")
return
self._conanfile.output.info(f"Making paths in .pth file {filename} relative")
with open(filename, "w") as f:
f.write("\n".join(lines) + "\n")
def _patch_egg_link(self, filename):
"""
Derived from https://github.com/pypa/virtualenv/blob/fb6e546cc1dfd0d363dc4d769486805d2d8f04bc/virtualenv.py#L2039-L2048
MIT License
:param filename: Path to the file to fix
:type filename: str
"""
with open(filename) as f:
link = f.readline().strip()
if os.path.abspath(link) != link:
self._debug(f"Link in {filename} already relative")
return
new_link = self._make_relative_path(filename, link)
self._conanfile.output.info(
"Rewriting link {} in {} as {}".format(link, filename, new_link)
)
with open(filename, "w") as f:
f.write(new_link)
def _make_relative_path(self, source, dest, dest_is_directory=True):
"""
Make a filename relative, where the filename is dest, and it is
being referred to from the filename source.
Derived from https://github.com/pypa/virtualenv/blob/fb6e546cc1dfd0d363dc4d769486805d2d8f04bc/virtualenv.py#L2051-L2084
MIT License
>>> make_relative_path('/usr/share/something/a-file.pth',
... '/usr/share/another-place/src/Directory')
'../another-place/src/Directory'
>>> make_relative_path('/usr/share/something/a-file.pth',
... '/home/user/src/Directory')
'../../../home/user/src/Directory'
>>> make_relative_path('/usr/share/a-file.pth', '/usr/share/')
'./'
:param source: The path from which the filename will be referred to
:type source: str
:param dest: The filename to be referred to from `source`
:type dest: str
:param dest_is_directory: Flag indicating whether `dest` is a directory. Defaults to `True`
:type dest: bool
"""
source = os.path.dirname(source)
if not dest_is_directory:
dest_filename = os.path.basename(dest)
dest = os.path.dirname(dest)
else:
dest_filename = None
dest = os.path.normpath(os.path.abspath(dest))
source = os.path.normpath(os.path.abspath(source))
dest_parts = dest.strip(os.path.sep).split(os.path.sep)
source_parts = source.strip(os.path.sep).split(os.path.sep)
while dest_parts and source_parts and dest_parts[0] == source_parts[0]:
dest_parts.pop(0)
source_parts.pop(0)
full_parts = [".."] * len(source_parts) + dest_parts
if not dest_is_directory and dest_filename is not None:
full_parts.append(dest_filename)
if not full_parts:
# Special case for the current directory (otherwise it'd be '')
return "./"
return os.path.sep.join(full_parts)
def _patch_activate_script(self, filename):
"""
Patch the virtualenvs activation scripts such that they discover the virtualenv path dynamically
rather than hardcoding it to make them robust to relocation.
:param filename: The filename to patch
:type bin_dir: str
"""
scripts = {
"activate": self._patch_activate,
"activate.sh": self._patch_activate,
"activate.bat": self._patch_activate_bat,
"activate.fish": self._patch_activate_fish,
"activate.csh": self._patch_activate_csh,
# pathlib.Path(bin_dir, "activate.xsh"): self._patch_activate_xsh,
# pathlib.Path(bin_dir, "activate.nu"): self._patch_activate_nu,
}
file_basename = os.path.basename(filename)
if file_basename in scripts:
patch = scripts[file_basename]
patch(filename)
def _patch_activate(self, activate):
"""
Patch a bash, sh, ksh, zsh or dash script such that it discovers the virtualenv path dynamically