Skip to content

Commit 2c4e482

Browse files
committed
Bundle the symlink_import() function with the package
Regular copy import is too slow for this package.
1 parent 4236bf6 commit 2c4e482

File tree

3 files changed

+65
-9
lines changed

3 files changed

+65
-9
lines changed

conanfile.py

+10-6
Original file line numberDiff line numberDiff line change
@@ -14,21 +14,22 @@ class EmbeddedPython(ConanFile):
1414
settings = {"os": ["Windows"]}
1515
options = {"pip_packages": "ANY"}
1616
default_options = "pip_packages=None"
17+
exports = "embedded_python_tools.py"
1718
short_paths = True # some of the pip packages go over the 260 char path limit on Windows
1819

1920
def _get_binaries(self):
2021
"""Get the binaries from the special embeddable Python package"""
2122
url = "https://www.python.org/ftp/python/{0}/python-{0}-embed-amd64.zip"
22-
tools.get(url.format(self.pyversion), md5=self.md5)
23+
tools.get(url.format(self.pyversion), md5=self.md5, destination="embedded_python")
2324

2425
def _get_headers_and_lib(self):
2526
"""We also need headers and the `python3.lib` file to link against"""
2627
url = "https://www.python.org/ftp/python/{0}/python-{0}-amd64-webinstall.exe"
2728
tools.download(url.format(self.pyversion), filename="tmp\\installer.exe")
2829
self.run("tmp\\installer.exe /quiet /layout")
29-
self.run("msiexec.exe /a {0}\\tmp\\dev.msi targetdir={0}".format(self.build_folder))
30+
dst = os.path.join(self.build_folder, "embedded_python")
31+
self.run(f"msiexec.exe /a {self.build_folder}\\tmp\\dev.msi targetdir={dst}")
3032
tools.rmdir("tmp")
31-
os.remove(self.build_folder + "\\dev.msi")
3233

3334
def build(self):
3435
self._get_binaries()
@@ -39,12 +40,15 @@ def build(self):
3940

4041
# Enable site-packages, i.e. additional non-system packages
4142
pyver = "".join(self.pyversion.split(".")[:2]) # e.g. 3.7.3 -> 37
42-
tools.replace_in_file("python{}._pth".format(pyver), "#import site", "import site")
43+
tools.replace_in_file("embedded_python/python{}._pth".format(pyver), "#import site", "import site")
4344

44-
target = self.build_folder + "/Lib/site-packages"
4545
packages = self.options.pip_packages.value
4646
packages += " setuptools==41.0.1" # some modules always assume it's installed (e.g. pytest)
47+
target = self.build_folder + "/embedded_python/Lib/site-packages"
4748
self.run(f'{sys.executable} -m pip install --no-deps --python-version {pyver} --target "{target}" {packages}')
4849

4950
def package(self):
50-
self.copy("*", dst="embedded_python", keep_path=True)
51+
self.copy("*", keep_path=True)
52+
53+
def package_info(self):
54+
self.env_info.PYTHONPATH.append(self.package_folder)

embedded_python_tools.py

+50
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
import os
2+
import shutil
3+
import pathlib
4+
5+
6+
def symlink_import(self, dst="bin/python/interpreter", bin="bin"):
7+
"""Copying the entire embedded Python environment is extremely slow, so we just symlink it
8+
9+
Usage:
10+
```python
11+
def imports(self):
12+
import embedded_python_tools
13+
embedded_python_tools.symlink_import(self, dst="bin/python/interpreter")
14+
```
15+
16+
On Windows, symlinks require admin privileges, so we use a directory junction instead.
17+
It points to the Conan package location. We still want to copy in `python*.dll` and
18+
`python*.zip` right next to the executable so that they can be found, but the rest of
19+
the Python environment is in a subfolder:
20+
21+
bin
22+
|- python/interpreter
23+
| |- Lib
24+
| \- ...
25+
|- <main>.exe
26+
|- python*.dll
27+
|- python*.zip
28+
\- ...
29+
"""
30+
if self.settings.os != "Windows":
31+
return
32+
33+
import _winapi
34+
35+
dst = pathlib.Path(dst).absolute()
36+
if not dst.parent.exists():
37+
dst.parent.mkdir(parents=True)
38+
39+
if dst.exists():
40+
try: # to remove any existing junction
41+
os.remove(dst)
42+
except: # this seems to be the only way to find out this is not a junction
43+
shutil.rmtree(dst)
44+
45+
src = pathlib.Path(self.deps_cpp_info["embedded_python"].rootpath) / "embedded_python"
46+
_winapi.CreateJunction(str(src), str(dst))
47+
48+
bin = pathlib.Path(bin).absolute()
49+
self.copy("python*.dll", dst=bin, src="embedded_python", keep_path=False)
50+
self.copy("python*.zip", dst=bin, src="embedded_python", keep_path=False)

test_package/conanfile.py

+5-3
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,13 @@
1+
import sys
12
from conans import ConanFile
23

34

45
class TestEmbeddedPython(ConanFile):
5-
settings = None
6+
settings = "os"
67

78
def imports(self):
8-
self.copy("*", dst="python", src="embedded_python")
9+
import embedded_python_tools
10+
embedded_python_tools.symlink_import(self, dst="bin/python")
911

1012
def test(self):
11-
self.run(".\\python\\python.exe --version")
13+
self.run(".\\bin\\python\\python.exe --version")

0 commit comments

Comments
 (0)