diff --git a/relenv/common.py b/relenv/common.py index 3cd99fa2..9c6c5bb7 100644 --- a/relenv/common.py +++ b/relenv/common.py @@ -543,6 +543,72 @@ def relative_interpreter(root_dir, scripts_dir, interpreter): return relscripts / relinterp +def makepath(*paths): + """ + Make a normalized path name from paths. + """ + dir = os.path.join(*paths) + try: + dir = os.path.abspath(dir) + except OSError: + pass + return dir, os.path.normcase(dir) + + +def addpackage(sitedir, name): + """ + Add editable package to path. + """ + import io + import stat + + fullname = os.path.join(sitedir, name) + paths = [] + try: + st = os.lstat(fullname) + except OSError: + return + if (getattr(st, "st_flags", 0) & stat.UF_HIDDEN) or ( + getattr(st, "st_file_attributes", 0) & stat.FILE_ATTRIBUTE_HIDDEN + ): + # print(f"Skipping hidden .pth file: {fullname!r}") + return + # print(f"Processing .pth file: {fullname!r}") + try: + # locale encoding is not ideal especially on Windows. But we have used + # it for a long time. setuptools uses the locale encoding too. + f = io.TextIOWrapper(io.open_code(fullname), encoding="locale") + except OSError: + return + with f: + for n, line in enumerate(f): + if line.startswith("#"): + continue + if line.strip() == "": + continue + try: + if line.startswith(("import ", "import\t")): + exec(line) + continue + line = line.rstrip() + dir, dircase = makepath(sitedir, line) + if dircase not in paths and os.path.exists(dir): + paths.append(dir) + except Exception: + print( + "Error processing line {:d} of {}:\n".format(n + 1, fullname), + file=sys.stderr, + ) + import traceback + + for record in traceback.format_exception(*sys.exc_info()): + for line in record.splitlines(): + print(" " + line, file=sys.stderr) + print("\nRemainder of file ignored", file=sys.stderr) + break + return paths + + def sanitize_sys_path(sys_path_entries): """ Sanitize `sys.path` to only include paths relative to the onedir environment. @@ -565,4 +631,10 @@ def sanitize_sys_path(sys_path_entries): if "PYTHONPATH" in os.environ: for __path in os.environ["PYTHONPATH"].split(os.pathsep): __sys_path.append(__path) + for known_path in __sys_path[:]: + for _ in pathlib.Path(known_path).glob("__editable__.*.pth"): + paths = addpackage(known_path, _) + for p in paths: + if p not in __sys_path: + __sys_path.append(p) return __sys_path diff --git a/tests/test_verify_build.py b/tests/test_verify_build.py index d85ef6a1..6d3c46c9 100644 --- a/tests/test_verify_build.py +++ b/tests/test_verify_build.py @@ -25,6 +25,28 @@ ] +EXTRAS_PY = """ +import pathlib +import sys + + +def setup(pth_file_path): + # Discover the extras-. directory + extras_parent_path = pathlib.Path(pth_file_path).resolve().parent.parent + if not sys.platform.startswith("win"): + extras_parent_path = extras_parent_path.parent + + extras_path = str(extras_parent_path / "extras-{}.{}".format(*sys.version_info)) + + if extras_path in sys.path and sys.path[0] != extras_path: + # The extras directory must come first + sys.path.remove(extras_path) + + if extras_path not in sys.path: + sys.path.insert(0, extras_path) +""" + + @pytest.fixture(scope="module") def arch(): return build_arch() @@ -1454,3 +1476,57 @@ def test_install_pyinotify_w_latest_pip(pipexec, build, minor_version): ) assert p.returncode == 0, "Failed install pyinotify" assert (extras / "pyinotify.py").exists() + + +@pytest.mark.skip_unless_on_linux +def test_install_editable_package(pipexec, pyexec, build, minor_version, tmp_path): + os.chdir(tmp_path) + env = os.environ.copy() + env["RELENV_BUILDENV"] = "yes" + p = subprocess.run( + [ + "git", + "clone", + "https://github.com/salt-extensions/saltext-zabbix.git", + "--depth", + "1", + ], + env=env, + ) + assert p.returncode == 0 + p = subprocess.run([str(pipexec), "install", "-e", "saltext-zabbix"], env=env) + assert p.returncode == 0 + p = subprocess.run([str(pyexec), "-c", "import saltext.zabbix"], env=env) + assert p.returncode == 0 + + +@pytest.mark.skip_unless_on_linux +def test_install_editable_package_in_extras( + pipexec, pyexec, build, minor_version, tmp_path +): + sitepkgs = pathlib.Path(build) / "lib" / f"python{minor_version}" / "site-packages" + + (sitepkgs / "_extras.pth").write_text("import _extras; _extras.setup(__file__)") + (sitepkgs / "_extras.py").write_text(EXTRAS_PY) + extras = pathlib.Path(build) / f"extras-{minor_version}" + extras.mkdir() + os.chdir(tmp_path) + env = os.environ.copy() + env["RELENV_BUILDENV"] = "yes" + p = subprocess.run( + [ + "git", + "clone", + "https://github.com/salt-extensions/saltext-zabbix.git", + "--depth", + "1", + ], + env=env, + ) + assert p.returncode == 0 + p = subprocess.run( + [str(pipexec), "install", f"--target={extras}", "-e", "saltext-zabbix"], env=env + ) + assert p.returncode == 0 + p = subprocess.run([str(pyexec), "-c", "import saltext.zabbix"], env=env) + assert p.returncode == 0