Skip to content

Commit 1cc548e

Browse files
committed
Honor pyvenv.cfg in the launcher as per PEP405
1 parent eb801a0 commit 1cc548e

File tree

5 files changed

+150
-39
lines changed

5 files changed

+150
-39
lines changed

graalpython/com.oracle.graal.python.shell/src/com/oracle/graal/python/shell/GraalPythonMain.java

Lines changed: 61 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,10 +25,12 @@
2525
*/
2626
package com.oracle.graal.python.shell;
2727

28+
import java.io.BufferedReader;
2829
import java.io.EOFException;
2930
import java.io.File;
3031
import java.io.FileDescriptor;
3132
import java.io.FileOutputStream;
33+
import java.io.IOError;
3234
import java.io.IOException;
3335
import java.io.InputStream;
3436
import java.io.OutputStream;
@@ -76,6 +78,8 @@ public static void main(String[] args) {
7678
// provided by GraalVM thin launcher
7779
protected static final String J_BASH_LAUNCHER_EXEC_PROPERTY_NAME = "org.graalvm.launcher.executablename";
7880

81+
private static final String J_PYENVCFG = "pyvenv.cfg";
82+
7983
private static long startupWallClockTime = -1;
8084
private static long startupNanoTime = -1;
8185

@@ -583,10 +587,21 @@ protected void launch(Builder contextBuilder) {
583587
if (executable != null) {
584588
contextBuilder.option("python.ExecutableList", executable);
585589
} else {
586-
contextBuilder.option("python.Executable", getExecutable());
590+
executable = getExecutable();
591+
contextBuilder.option("python.Executable", executable);
587592
// The unlikely separator is used because options need to be
588593
// strings. See PythonOptions.getExecutableList()
589594
contextBuilder.option("python.ExecutableList", String.join("🏆", getExecutableList()));
595+
// We try locating and loading options from pyvenv.cfg according to PEP405 as long as
596+
// the user did not explicitly pass some options that would be otherwise loaded from
597+
// pyvenv.cfg. Notable usage of this feature is GraalPython venvs which generate a
598+
// launcher script that passes those options explicitly without relying on pyvenv.cfg
599+
boolean tryVenvCfg = !hasContextOptionSetViaCommandLine("SysPrefix") &&
600+
!hasContextOptionSetViaCommandLine("PythonHome") &&
601+
System.getenv("GRAAL_PYTHONHOME") == null;
602+
if (tryVenvCfg) {
603+
findAndApplyVenvCfg(contextBuilder, executable);
604+
}
590605
}
591606

592607
// setting this to make sure our TopLevelExceptionHandler calls the excepthook
@@ -684,6 +699,39 @@ protected void launch(Builder contextBuilder) {
684699
System.exit(rc);
685700
}
686701

702+
private void findAndApplyVenvCfg(Builder contextBuilder, String executable) {
703+
Path binDir = Paths.get(executable).getParent();
704+
Path venvCfg = binDir.resolve(J_PYENVCFG);
705+
if (!Files.exists(venvCfg)) {
706+
venvCfg = binDir.getParent().resolve(J_PYENVCFG);
707+
if (!Files.exists(venvCfg)) {
708+
return; // not found
709+
}
710+
}
711+
try (BufferedReader reader = Files.newBufferedReader(venvCfg)) {
712+
String line;
713+
while ((line = reader.readLine()) != null) {
714+
String[] parts = line.split("=", 2);
715+
if (parts.length != 2) {
716+
continue;
717+
}
718+
String name = parts[0].trim();
719+
if (name.equals("home")) {
720+
contextBuilder.option("python.PythonHome", parts[1].trim());
721+
try {
722+
contextBuilder.option("python.SysPrefix", venvCfg.getParent().toAbsolutePath().toString());
723+
} catch (IOError ex) {
724+
System.err.println();
725+
throw abort("Could not set the home according to the pyvenv.cfg file.", 65);
726+
}
727+
break;
728+
}
729+
}
730+
} catch (IOException ex) {
731+
throw abort("Could not read the pyvenv.cfg file.", 66);
732+
}
733+
}
734+
687735
private static boolean matchesPythonOption(String arg, String key) {
688736
assert !key.startsWith("python.");
689737
return arg.startsWith("--python." + key) || arg.startsWith("--" + key);
@@ -715,6 +763,18 @@ private String getContextOptionIfSetViaCommandLine(String key) {
715763
return null;
716764
}
717765

766+
private boolean hasContextOptionSetViaCommandLine(String key) {
767+
if (System.getProperty("polyglot.python." + key) != null) {
768+
return System.getProperty("polyglot.python." + key) != null;
769+
}
770+
for (String f : givenArguments) {
771+
if (matchesPythonOption(f, key)) {
772+
return true;
773+
}
774+
}
775+
return false;
776+
}
777+
718778
private static void printFileNotFoundException(NoSuchFileException e) {
719779
String reason = e.getReason();
720780
if (reason == null) {

graalpython/com.oracle.graal.python/src/com/oracle/graal/python/builtins/modules/GraalPythonModuleBuiltins.java

Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -499,6 +499,77 @@ public Object doIt(PFunction func,
499499
}
500500
}
501501

502+
@Builtin(name = "get_toolchain_tools_for_venv")
503+
@TypeSystemReference(PythonArithmeticTypes.class)
504+
@GenerateNodeFactory
505+
public abstract static class GetToolchainToolsForVenv extends PythonBuiltinNode {
506+
private static final class Tool {
507+
final String name;
508+
final boolean isVariableName;
509+
final Object[] targets;
510+
511+
public Tool(String name, boolean isVariableName, Object[] targets) {
512+
this.name = name;
513+
this.isVariableName = isVariableName;
514+
this.targets = targets;
515+
}
516+
517+
static Tool forVariable(String name, Object... targets) {
518+
return new Tool(name, true, targets);
519+
}
520+
521+
static Tool forBinary(String name, Object... targets) {
522+
return new Tool(name, true, targets);
523+
}
524+
}
525+
526+
static final Tool[] tools = new Tool[]{
527+
Tool.forVariable("AR", tsLiteral("ar")),
528+
Tool.forVariable("RANLIB", tsLiteral("ranlib")),
529+
Tool.forVariable("NM", tsLiteral("nm")),
530+
Tool.forVariable("LD", tsLiteral("ld.lld"), tsLiteral("ld"), tsLiteral("lld")),
531+
Tool.forVariable("CC", tsLiteral("clang"), tsLiteral("cc")),
532+
Tool.forVariable("CXX", tsLiteral("clang++"), tsLiteral("c++")),
533+
Tool.forBinary("llvm-as", tsLiteral("as")),
534+
Tool.forBinary("clang-cl", tsLiteral("cl")),
535+
Tool.forBinary("clang-cpp", tsLiteral("cpp")),
536+
};
537+
538+
@Specialization
539+
@TruffleBoundary
540+
protected Object getToolPath() {
541+
Env env = getContext().getEnv();
542+
LanguageInfo llvmInfo = env.getInternalLanguages().get(J_LLVM_LANGUAGE);
543+
Toolchain toolchain = env.lookup(llvmInfo, Toolchain.class);
544+
List<TruffleFile> toolchainPaths = toolchain.getPaths("PATH");
545+
EconomicMapStorage storage = EconomicMapStorage.create(tools.length);
546+
for (Tool tool : tools) {
547+
String path = null;
548+
if (tool.isVariableName) {
549+
TruffleFile toolPath = toolchain.getToolPath(tool.name);
550+
if (toolPath != null) {
551+
path = toolPath.getAbsoluteFile().getPath();
552+
}
553+
} else {
554+
for (TruffleFile toolchainPath : toolchainPaths) {
555+
LOGGER.finest(() -> " Testing path " + toolchainPath.getPath() + " for tool " + tool.name);
556+
TruffleFile pathToTest = toolchainPath.resolve(tool.name);
557+
if (pathToTest.exists()) {
558+
path = pathToTest.getAbsoluteFile().getPath();
559+
break;
560+
}
561+
}
562+
}
563+
if (path != null) {
564+
storage.putUncached(toTruffleStringUncached(path), factory().createTuple(tool.targets));
565+
} else {
566+
LOGGER.info("Could not locate tool " + tool.name);
567+
}
568+
}
569+
return factory().createDict(storage);
570+
}
571+
}
572+
502573
/*
503574
* Internal check used in tests only to check that we are running through managed launcher.
504575
*/

graalpython/com.oracle.graal.python/src/com/oracle/graal/python/runtime/PythonContext.java

Lines changed: 9 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1551,20 +1551,22 @@ public void initializeHomeAndPrefixPaths(Env newEnv, String languageHome) {
15511551
"\n\tCAPI: {5}" +
15521552
"\n\tJNI library: {6}", languageHome, sysPrefix, basePrefix, coreHome, stdLibHome, capiHome, jniHome));
15531553

1554-
String envHome = null;
1555-
try {
1556-
envHome = System.getenv("GRAAL_PYTHONHOME");
1557-
} catch (SecurityException e) {
1554+
String pythonHome = newEnv.getOptions().get(PythonOptions.PythonHome);
1555+
if (pythonHome.isEmpty()) {
1556+
try {
1557+
pythonHome = System.getenv("GRAAL_PYTHONHOME");
1558+
} catch (SecurityException e) {
1559+
}
15581560
}
15591561

15601562
final TruffleFile home;
1561-
if (languageHome != null && envHome == null) {
1563+
if (languageHome != null && pythonHome == null) {
15621564
home = newEnv.getInternalTruffleFile(languageHome);
1563-
} else if (envHome != null) {
1565+
} else if (pythonHome != null) {
15641566
boolean envHomeIsDirectory = false;
15651567
TruffleFile envHomeFile = null;
15661568
try {
1567-
envHomeFile = newEnv.getInternalTruffleFile(envHome);
1569+
envHomeFile = newEnv.getInternalTruffleFile(pythonHome);
15681570
envHomeIsDirectory = envHomeFile.isDirectory();
15691571
} catch (SecurityException e) {
15701572
}

graalpython/com.oracle.graal.python/src/com/oracle/graal/python/runtime/PythonOptions.java

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -92,6 +92,10 @@ private PythonOptions() {
9292
// no instances
9393
}
9494

95+
@Option(category = OptionCategory.USER, help = "Set the home of Python. Equivalent of GRAAL_PYTHONHOME env variable. " +
96+
"Determines default values for the CoreHome, StdLibHome, SysBasePrefix, SysPrefix.", usageSyntax = "<path>", stability = OptionStability.STABLE) //
97+
public static final OptionKey<String> PythonHome = new OptionKey<>("");
98+
9599
@Option(category = OptionCategory.USER, help = "Set the location of sys.prefix. Overrides any environment variables or Java options.", usageSyntax = "<path>", stability = OptionStability.STABLE) //
96100
public static final OptionKey<TruffleString> SysPrefix = new OptionKey<>(T_EMPTY_STRING, TS_OPTION_TYPE);
97101

graalpython/lib-python/3/venv/__init__.py

Lines changed: 5 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -187,38 +187,12 @@ def create_if_needed(d):
187187

188188
def _install_compilers(self, context):
189189
"""Puts the Graal LLVM compiler tools on the path"""
190-
191-
# Table of well-known LLVM tools that must be queried by a variable name.
192-
llvm_tools = {
193-
"AR": ("ar",),
194-
"RANLIB": ("ranlib",),
195-
"NM": ("nm",),
196-
"LD": ("ld.lld", "ld", "lld"),
197-
"CC": ("clang", "cc"),
198-
"CXX": ("clang++", "c++"),
199-
}
200-
# Table of additional LLVM tools to use if they are available.
201-
_llvm_bins = {
202-
"llvm-as": ("as",),
203-
"clang-cl": ("cl",),
204-
"clang-cpp": ("cpp",),
205-
}
206190
bin_dir = os.path.join(context.env_dir, context.bin_name)
207-
def create_symlinks(table, resolver):
208-
for tool_var in table:
209-
tool_path = resolver(tool_var)
210-
if os.path.exists(tool_path):
211-
for name in table[tool_var]:
212-
dest = os.path.join(bin_dir, name)
213-
if not os.path.exists(dest):
214-
os.symlink(tool_path, dest)
215-
216-
create_symlinks(llvm_tools, __graalpython__.get_toolchain_tool_path)
217-
# NOTE: function 'get_toolcahin_paths' returns a tuple
218-
llvm_path = __graalpython__.get_toolchain_paths("PATH")
219-
if llvm_path and llvm_path[0]:
220-
create_symlinks(_llvm_bins, lambda binary_name: os.path.join(llvm_path[0], binary_name))
221-
191+
for (tool_path, names) in __graalpython__.get_toolchain_tools_for_venv().items():
192+
for name in names:
193+
dest = os.path.join(bin_dir, name)
194+
if not os.path.exists(dest):
195+
os.symlink(tool_path, dest)
222196

223197
def _patch_shebang(self, context):
224198
# Truffle change: we need to patch the pip/pip3 (and maybe other)

0 commit comments

Comments
 (0)