Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -40,11 +40,15 @@
*/
package com.oracle.graal.python.test.integration.advanced;

import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.concurrent.CountDownLatch;

import org.graalvm.polyglot.Context;
import org.graalvm.polyglot.Engine;
import org.graalvm.polyglot.PolyglotException;
import org.graalvm.polyglot.Value;
import org.junit.Assert;
import org.junit.Assume;
import org.junit.BeforeClass;
Expand All @@ -55,6 +59,8 @@
* because we cannot create multiple contexts that would load native extensions.
*/
public class NativeExtTest {
private static final String DELVEWHEEL_VERSION = "1.9.0";

@BeforeClass
public static void setUpClass() {
Assume.assumeFalse(System.getProperty("os.name").toLowerCase().contains("mac"));
Expand Down Expand Up @@ -109,6 +115,52 @@ public void testSharingErrorWithCpythonSre() throws InterruptedException {
}
}

@Test
public void testMissingDelvewheelError() throws IOException {
Assume.assumeTrue(System.getProperty("os.name").toLowerCase().contains("win"));

Path tempDir = Files.createTempDirectory("graalpy-no-delvewheel");
try (Engine engine = Engine.create("python");
Context context = newContext(engine).option("python.IsolateNativeModules", "true").option("python.Executable", tempDir.resolve("python.exe").toString()).environment("PATH",
tempDir.toString()).build()) {
try {
context.eval("python", "import _sqlite3");
Assert.fail("importing _sqlite3 with python.IsolateNativeModules=true should fail when delvewheel is not on PATH");
} catch (PolyglotException ex) {
Assert.assertTrue(ex.getMessage(), ex.isGuestException());
Value exception = ex.getGuestObject();
Assert.assertTrue(exception.isException());
Assert.assertEquals(ex.getMessage(), "SystemError", exception.getMetaObject().getMetaSimpleName());
Assert.assertTrue(ex.getMessage(), ex.getMessage().contains("delvewheel==" + DELVEWHEEL_VERSION));
}
} finally {
Files.deleteIfExists(tempDir);
}
}

@Test
public void testMissingPatchelfError() throws IOException {
Assume.assumeTrue(System.getProperty("os.name").toLowerCase().contains("linux"));

Path tempDir = Files.createTempDirectory("graalpy-no-patchelf");
try (Engine engine = Engine.create("python");
Context context = newContext(engine).option("python.IsolateNativeModules", "true").option("python.Executable", tempDir.resolve("python").toString()).environment("PATH",
tempDir.toString()).build()) {
try {
context.eval("python", "import _sqlite3");
Assert.fail("importing _sqlite3 with python.IsolateNativeModules=true should fail when patchelf is not on PATH");
} catch (PolyglotException ex) {
Assert.assertTrue(ex.getMessage(), ex.isGuestException());
Value exception = ex.getGuestObject();
Assert.assertTrue(exception.isException());
Assert.assertEquals(ex.getMessage(), "SystemError", exception.getMetaObject().getMetaSimpleName());
Assert.assertTrue(ex.getMessage(), ex.getMessage().contains("patchelf`"));
}
} finally {
Files.deleteIfExists(tempDir);
}
}

private static Context.Builder newContext(Engine engine) {
return Context.newBuilder().allowExperimentalOptions(true).allowAllAccess(true).engine(engine);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -110,6 +110,7 @@
import com.oracle.graal.python.builtins.objects.cext.capi.transitions.CApiTransitions.HandlePointerConverter;
import com.oracle.graal.python.builtins.objects.cext.capi.transitions.CApiTransitions.PythonObjectReference;
import com.oracle.graal.python.builtins.objects.cext.copying.NativeLibraryLocator;
import com.oracle.graal.python.builtins.objects.cext.copying.NativeLibraryToolException;
import com.oracle.graal.python.builtins.objects.cext.structs.CFields;
import com.oracle.graal.python.builtins.objects.cext.structs.CStructAccess;
import com.oracle.graal.python.builtins.objects.code.CodeNodes;
Expand Down Expand Up @@ -1408,7 +1409,7 @@ static Object replicate(TruffleString venvPath, int count,
@Bind PythonContext context) {
try {
NativeLibraryLocator.replicate(context.getEnv().getPublicTruffleFile(venvPath.toJavaStringUncached()), context, count);
} catch (IOException | InterruptedException e) {
} catch (NativeLibraryToolException e) {
throw PRaiseNode.raiseStatic(node, PythonBuiltinClassType.ValueError, e);
}
return PNone.NONE;
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/*
* Copyright (c) 2024, 2025, Oracle and/or its affiliates. All rights reserved.
* Copyright (c) 2024, 2026, Oracle and/or its affiliates. All rights reserved.
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
*
* The Universal Permissive License (UPL), Version 1.0
Expand Down Expand Up @@ -41,61 +41,122 @@

package com.oracle.graal.python.builtins.objects.cext.copying;

import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.nio.file.StandardCopyOption;
import java.nio.file.StandardOpenOption;

import com.oracle.graal.python.runtime.PythonContext;
import com.oracle.truffle.api.TruffleFile;

final class ElfFile extends SharedObject {
private static final String PATCHELF_INSTALL_INSTRUCTION = "IsolateNativeModules option needs `patchelf` tool to copy libraries. Make sure you have it available " +
"on PATH or installed in your venv.";

private final PythonContext context;
private final TruffleFile tempfile;

private String getPatchelf() {
return which(context, "patchelf").toString();
private String getPatchelf() throws NativeLibraryToolException {
TruffleFile patchelf = which(context, "patchelf");
if (!patchelf.exists()) {
throw new NativeLibraryToolException("Could not find `patchelf`. " + PATCHELF_INSTALL_INSTRUCTION);
}
return patchelf.toString();
}

ElfFile(byte[] b, PythonContext context) throws IOException {
this.context = context;
this.tempfile = context.getEnv().createTempFile(null, null, ".so");
try (var os = this.tempfile.newOutputStream(StandardOpenOption.CREATE, StandardOpenOption.TRUNCATE_EXISTING, StandardOpenOption.WRITE)) {
os.write(b);
private void runPatchelf(String action, String... arguments) throws NativeLibraryToolException {
var command = new String[arguments.length + 1];
command[0] = getPatchelf();
System.arraycopy(arguments, 0, command, 1, arguments.length);
var pb = newProcessBuilder(context);
var stderr = new ByteArrayOutputStream();
pb.redirectError(pb.createRedirectToStream(stderr));
pb.command(command);
Process proc;
try {
proc = pb.start();
} catch (IOException e) {
throw new NativeLibraryToolException("Failed to start `patchelf` to " + action + ": " + e.getMessage() + ". " + PATCHELF_INSTALL_INSTRUCTION, e);
}
try {
if (proc.waitFor() != 0) {
throw new NativeLibraryToolException("Failed to run `patchelf` to " + action + " (exit code " + proc.exitValue() + "). " + PATCHELF_INSTALL_INSTRUCTION +
" Stderr: " + getStderr(stderr));
}
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
throw new NativeLibraryToolException("Interrupted while waiting for `patchelf` to " + action + ". " + PATCHELF_INSTALL_INSTRUCTION, e);
}
}

@Override
public void setId(String newId) throws IOException, InterruptedException {
var pb = newProcessBuilder(context);
pb.command(getPatchelf(), "--debug", "--set-soname", newId, tempfile.toString());
var proc = pb.start();
if (proc.waitFor() != 0) {
throw new IOException("Failed to run `patchelf` command. Make sure you have it on your PATH or installed in your venv.");
private static String getStderr(ByteArrayOutputStream stderr) {
String output = stderr.toString(StandardCharsets.UTF_8).strip();
return output.isEmpty() ? "<empty>" : output;
}

private static void deleteTempfile(TruffleFile tempfile) throws NativeLibraryToolException {
try {
tempfile.delete();
} catch (IOException e) {
throw new NativeLibraryToolException("Failed to delete temporary ELF library copy '" + tempfile + "': " + e.getMessage(), e);
}
}

@Override
public void changeOrAddDependency(String oldName, String newName) throws IOException, InterruptedException {
var pb = newProcessBuilder(context);
pb.command(getPatchelf(), "--debug", "--remove-needed", oldName, tempfile.toString());
var proc = pb.start();
if (proc.waitFor() != 0) {
throw new IOException("Failed to run `patchelf` command. Make sure you have it on your PATH or installed in your venv.");
private static void deleteTempfileAfterFailedInit(TruffleFile tempfile, IOException failure) throws NativeLibraryToolException {
var exception = new NativeLibraryToolException("Failed to write temporary ELF library copy '" + tempfile + "' for IsolateNativeModules relocation: " + failure.getMessage(), failure);
try {
deleteTempfile(tempfile);
} catch (NativeLibraryToolException e) {
exception.addSuppressed(e);
}
throw exception;
}

private static void writeTempfile(TruffleFile tempfile, byte[] b) throws NativeLibraryToolException {
try (var os = tempfile.newOutputStream(StandardOpenOption.CREATE, StandardOpenOption.TRUNCATE_EXISTING, StandardOpenOption.WRITE)) {
os.write(b);
} catch (IOException e) {
deleteTempfileAfterFailedInit(tempfile, e);
}
pb.command(getPatchelf(), "--debug", "--add-needed", newName, tempfile.toString());
proc = pb.start();
if (proc.waitFor() != 0) {
throw new IOException("Failed to run `patchelf` command. Make sure you have it on your PATH or installed in your venv.");
}

private static TruffleFile createTempfile(PythonContext context) throws NativeLibraryToolException {
try {
return context.getEnv().createTempFile(null, null, ".so");
} catch (IOException e) {
throw new NativeLibraryToolException("Failed to create temporary ELF library copy for IsolateNativeModules relocation: " + e.getMessage(), e);
}
}

ElfFile(byte[] b, PythonContext context) throws NativeLibraryToolException {
this.context = context;
this.tempfile = createTempfile(context);
writeTempfile(tempfile, b);
}

@Override
public void write(TruffleFile copy) throws IOException {
tempfile.copy(copy, StandardCopyOption.REPLACE_EXISTING, StandardCopyOption.COPY_ATTRIBUTES);
public void setId(String newId) throws NativeLibraryToolException {
runPatchelf("set SONAME", "--debug", "--set-soname", newId, tempfile.toString());
}

@Override
public void changeOrAddDependency(String oldName, String newName) throws NativeLibraryToolException {
runPatchelf("remove dependency", "--debug", "--remove-needed", oldName, tempfile.toString());
runPatchelf("add dependency", "--debug", "--add-needed", newName, tempfile.toString());
}

@Override
public void write(TruffleFile copy) throws NativeLibraryToolException {
try {
tempfile.copy(copy, StandardCopyOption.REPLACE_EXISTING, StandardCopyOption.COPY_ATTRIBUTES);
} catch (IOException e) {
throw new NativeLibraryToolException("Failed to write relocated ELF library copy '" + copy + "': " + e.getMessage(), e);
}
}

@Override
public void close() throws IOException {
tempfile.delete();
public void close() throws NativeLibraryToolException {
deleteTempfile(tempfile);
}
}
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/*
* Copyright (c) 2024, 2025, Oracle and/or its affiliates. All rights reserved.
* Copyright (c) 2024, 2026, Oracle and/or its affiliates. All rights reserved.
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
*
* The Universal Permissive License (UPL), Version 1.0
Expand Down Expand Up @@ -40,18 +40,17 @@
*/
package com.oracle.graal.python.builtins.objects.cext.copying;

import static com.oracle.graal.python.nodes.StringLiterals.J_MAX_CAPI_COPIES;
import static com.oracle.graal.python.nodes.StringLiterals.J_NATIVE;
import static com.oracle.graal.python.nodes.StringLiterals.T_BASE_PREFIX;
import static com.oracle.graal.python.nodes.StringLiterals.J_MAX_CAPI_COPIES;
import static com.oracle.graal.python.nodes.StringLiterals.T_PREFIX;
import static com.oracle.graal.python.util.PythonUtils.toTruffleStringUncached;
import static com.oracle.graal.python.util.PythonUtils.TS_ENCODING;

import java.io.IOException;
import java.util.concurrent.atomic.AtomicInteger;

import com.oracle.graal.python.PythonLanguage;
import com.oracle.graal.python.builtins.objects.cext.common.LoadCExtException.ApiInitException;
import com.oracle.graal.python.builtins.objects.cext.common.LoadCExtException.ImportException;
import com.oracle.graal.python.nodes.ErrorMessages;
import com.oracle.graal.python.nodes.util.CannotCastException;
import com.oracle.graal.python.nodes.util.CastToTruffleStringNode;
Expand All @@ -61,6 +60,7 @@
import com.oracle.truffle.api.TruffleFile;
import com.oracle.truffle.api.TruffleLanguage.Env;
import com.oracle.truffle.api.TruffleLogger;
import com.oracle.truffle.api.strings.TruffleString;

/**
* Given a GraalPy virtual environment, this class helps prepare that environment so that multiple
Expand Down Expand Up @@ -127,13 +127,8 @@ public NativeLibraryLocator(PythonContext context, TruffleFile capiLibrary, bool
*
* @see PythonOptions#IsolateNativeModules
*/
public String resolve(PythonContext context, TruffleFile original) throws ImportException {
try {
return resolve(context, original, capiSlot, capiOriginal);
} catch (ApiInitException e) {
throw new ImportException(null, toTruffleStringUncached(original.getName()), toTruffleStringUncached(original.getPath()),
toTruffleStringUncached(e.getMessage() == null ? "" : e.getMessage()));
}
public String resolve(PythonContext context, TruffleFile original) throws ApiInitException {
return resolve(context, original, capiSlot, capiOriginal);
}

public String getCapiLibrary() {
Expand All @@ -148,32 +143,21 @@ public void close() {
* the same time. The minimum number of concurrent contexts to prepare for is given with {@code
* count}.
*/
public static void replicate(TruffleFile venvDirectory, PythonContext context, int count) throws IOException, InterruptedException {
public static void replicate(TruffleFile venvDirectory, PythonContext context, int count) throws NativeLibraryToolException {
if (count > MAX_CEXT_COPIES) {
LOGGER.warning(() -> String.format("The current limit for concurrent Python contexts accessing the Python C API is %d, " +
"but we are preparing %d copies. The extra copies will only be used if a different value " +
"of the system property %s is set.", MAX_CEXT_COPIES, count, J_MAX_CAPI_COPIES));
}
String suffix = context.getSoAbi().toJavaStringUncached();
TruffleFile capiLibrary = context.getPublicTruffleFileRelaxed(context.getCAPIHome()).resolve(PythonContext.getSupportLibName("python-" + J_NATIVE));
try {
for (int i = 0; i < count; i++) {
// Relocate the C API library
replicate(capiLibrary, venvDirectory.resolve(copyNameOf(capiLibrary.getName(), i)), context, i);
// Relocate the core C extensions
walk(context.getPublicTruffleFileRelaxed(context.getCoreHome()), suffix, capiLibrary.getName(), context, i, (o, n) -> venvDirectory.resolve(n));
// Relocate C extensions in the venv
walk(venvDirectory, suffix, capiLibrary.getName(), context, i, (o, n) -> o.resolveSibling(n));
}
} catch (RuntimeException e) {
var cause = e.getCause();
if (cause instanceof IOException ioCause) {
throw ioCause;
} else if (cause instanceof InterruptedException intCause) {
throw intCause;
} else {
throw e;
}
for (int i = 0; i < count; i++) {
// Relocate the C API library
replicate(capiLibrary, venvDirectory.resolve(copyNameOf(capiLibrary.getName(), i)), context, i);
// Relocate the core C extensions
walk(context.getPublicTruffleFileRelaxed(context.getCoreHome()), suffix, capiLibrary.getName(), context, i, (o, n) -> venvDirectory.resolve(n));
// Relocate C extensions in the venv
walk(venvDirectory, suffix, capiLibrary.getName(), context, i, (o, n) -> o.resolveSibling(n));
}
}

Expand Down Expand Up @@ -229,14 +213,15 @@ private static String resolve(PythonContext context, TruffleFile original, int c
if (!copy.isReadable()) {
try {
replicate(original, copy, context, capiSlot, capiOrignalName);
} catch (IOException | InterruptedException e) {
throw new ApiInitException(e);
} catch (NativeLibraryToolException e) {
throw new ApiInitException(TruffleString.fromJavaStringUncached(e.getMessage(), TS_ENCODING));
}
}
return copy.getPath();
}

private static void replicate(TruffleFile original, TruffleFile copy, PythonContext context, int slot, String... dependenciesToUpdate) throws IOException, InterruptedException {
private static void replicate(TruffleFile original, TruffleFile copy, PythonContext context, int slot, String... dependenciesToUpdate)
throws NativeLibraryToolException {
try (var o = SharedObject.open(original, context)) {
for (var depToUpdate : dependenciesToUpdate) {
if (depToUpdate != null) {
Expand All @@ -250,7 +235,7 @@ private static void replicate(TruffleFile original, TruffleFile copy, PythonCont
}

private static void walk(TruffleFile dir, String suffix, String capiOriginalName, PythonContext context, int capiSlot, BiFunction<TruffleFile, String, TruffleFile> f)
throws IOException, InterruptedException {
throws NativeLibraryToolException {
try (var ds = dir.newDirectoryStream()) {
for (var e : ds) {
if (e.isDirectory()) {
Expand All @@ -259,6 +244,8 @@ private static void walk(TruffleFile dir, String suffix, String capiOriginalName
replicate(e, f.apply(e, copyNameOf(e.getName(), capiSlot)), context, capiSlot, capiOriginalName);
}
}
} catch (IOException e) {
throw new NativeLibraryToolException("Failed to scan native library directory '" + dir + "' for IsolateNativeModules relocation: " + e.getMessage(), e);
}
}
}
Loading
Loading