diff --git a/graalpython/com.oracle.graal.python.test.integration/src/com/oracle/graal/python/test/integration/advanced/NativeExtTest.java b/graalpython/com.oracle.graal.python.test.integration/src/com/oracle/graal/python/test/integration/advanced/NativeExtTest.java index c9322f6b0b..1dc193ed0d 100644 --- a/graalpython/com.oracle.graal.python.test.integration/src/com/oracle/graal/python/test/integration/advanced/NativeExtTest.java +++ b/graalpython/com.oracle.graal.python.test.integration/src/com/oracle/graal/python/test/integration/advanced/NativeExtTest.java @@ -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; @@ -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")); @@ -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); } diff --git a/graalpython/com.oracle.graal.python/src/com/oracle/graal/python/builtins/modules/GraalPythonModuleBuiltins.java b/graalpython/com.oracle.graal.python/src/com/oracle/graal/python/builtins/modules/GraalPythonModuleBuiltins.java index 919f463a0a..8af0696915 100644 --- a/graalpython/com.oracle.graal.python/src/com/oracle/graal/python/builtins/modules/GraalPythonModuleBuiltins.java +++ b/graalpython/com.oracle.graal.python/src/com/oracle/graal/python/builtins/modules/GraalPythonModuleBuiltins.java @@ -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; @@ -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; diff --git a/graalpython/com.oracle.graal.python/src/com/oracle/graal/python/builtins/objects/cext/copying/ElfFile.java b/graalpython/com.oracle.graal.python/src/com/oracle/graal/python/builtins/objects/cext/copying/ElfFile.java index ee6c5c0e65..c813edf84a 100644 --- a/graalpython/com.oracle.graal.python/src/com/oracle/graal/python/builtins/objects/cext/copying/ElfFile.java +++ b/graalpython/com.oracle.graal.python/src/com/oracle/graal/python/builtins/objects/cext/copying/ElfFile.java @@ -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 @@ -41,7 +41,9 @@ 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; @@ -49,53 +51,112 @@ 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() ? "" : 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); } } diff --git a/graalpython/com.oracle.graal.python/src/com/oracle/graal/python/builtins/objects/cext/copying/NativeLibraryLocator.java b/graalpython/com.oracle.graal.python/src/com/oracle/graal/python/builtins/objects/cext/copying/NativeLibraryLocator.java index 4366027daf..db4aa1d54f 100644 --- a/graalpython/com.oracle.graal.python/src/com/oracle/graal/python/builtins/objects/cext/copying/NativeLibraryLocator.java +++ b/graalpython/com.oracle.graal.python/src/com/oracle/graal/python/builtins/objects/cext/copying/NativeLibraryLocator.java @@ -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 @@ -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; @@ -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 @@ -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() { @@ -148,7 +143,7 @@ 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 " + @@ -156,24 +151,13 @@ public static void replicate(TruffleFile venvDirectory, PythonContext context, i } 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)); } } @@ -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) { @@ -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 f) - throws IOException, InterruptedException { + throws NativeLibraryToolException { try (var ds = dir.newDirectoryStream()) { for (var e : ds) { if (e.isDirectory()) { @@ -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); } } } diff --git a/graalpython/com.oracle.graal.python/src/com/oracle/graal/python/builtins/objects/cext/copying/NativeLibraryToolException.java b/graalpython/com.oracle.graal.python/src/com/oracle/graal/python/builtins/objects/cext/copying/NativeLibraryToolException.java new file mode 100644 index 0000000000..f3be9f34da --- /dev/null +++ b/graalpython/com.oracle.graal.python/src/com/oracle/graal/python/builtins/objects/cext/copying/NativeLibraryToolException.java @@ -0,0 +1,53 @@ +/* + * Copyright (c) 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 + * + * Subject to the condition set forth below, permission is hereby granted to any + * person obtaining a copy of this software, associated documentation and/or + * data (collectively the "Software"), free of charge and under any and all + * copyright rights in the Software, and any and all patent rights owned or + * freely licensable by each licensor hereunder covering either (i) the + * unmodified Software as contributed to or provided by such licensor, or (ii) + * the Larger Works (as defined below), to deal in both + * + * (a) the Software, and + * + * (b) any piece of software and/or hardware listed in the lrgrwrks.txt file if + * one is included with the Software each a "Larger Work" to which the Software + * is contributed by such licensors), + * + * without restriction, including without limitation the rights to copy, create + * derivative works of, display, perform, and distribute the Software and make, + * use, sell, offer for sale, import, export, have made, and have sold the + * Software and the Larger Work(s), and to sublicense the foregoing rights on + * either these or other terms. + * + * This license is subject to the following condition: + * + * The above copyright notice and either this complete permission notice or at a + * minimum a reference to the UPL must be included in all copies or substantial + * portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ +package com.oracle.graal.python.builtins.objects.cext.copying; + +public final class NativeLibraryToolException extends Exception { + private static final long serialVersionUID = 5478160327398513738L; + + NativeLibraryToolException(String message) { + super(message); + } + + NativeLibraryToolException(String message, Throwable cause) { + super(message, cause); + } +} diff --git a/graalpython/com.oracle.graal.python/src/com/oracle/graal/python/builtins/objects/cext/copying/PEFile.java b/graalpython/com.oracle.graal.python/src/com/oracle/graal/python/builtins/objects/cext/copying/PEFile.java index 63cbd39ea3..6227705191 100644 --- a/graalpython/com.oracle.graal.python/src/com/oracle/graal/python/builtins/objects/cext/copying/PEFile.java +++ b/graalpython/com.oracle.graal.python/src/com/oracle/graal/python/builtins/objects/cext/copying/PEFile.java @@ -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 @@ -41,7 +41,9 @@ 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; @@ -49,23 +51,35 @@ import com.oracle.truffle.api.TruffleFile; final class PEFile extends SharedObject { + private static final String DELVEWHEEL_VERSION = "1.9.0"; + private static final String DELVEWHEEL_INSTALL_INSTRUCTION = "IsolateNativeModules option needs `delvewheel` tool to copy libraries. Make sure you have `delvewheel==" + DELVEWHEEL_VERSION + + "` available in the virtualenv or on PATH (needs environment access)."; + private final PythonContext context; private final TruffleFile tempfile; - PEFile(byte[] b, PythonContext context) throws IOException { + PEFile(byte[] b, PythonContext context) throws NativeLibraryToolException { this.context = context; - this.tempfile = context.getEnv().createTempFile(null, null, ".dll"); - try (var os = this.tempfile.newOutputStream(StandardOpenOption.CREATE, StandardOpenOption.TRUNCATE_EXISTING, StandardOpenOption.WRITE)) { + TruffleFile temp; + try { + temp = context.getEnv().createTempFile(null, null, ".dll"); + } catch (IOException e) { + throw new NativeLibraryToolException("Failed to create temporary PE library copy for IsolateNativeModules relocation: " + e.getMessage(), e); + } + this.tempfile = temp; + try (var os = tempfile.newOutputStream(StandardOpenOption.CREATE, StandardOpenOption.TRUNCATE_EXISTING, StandardOpenOption.WRITE)) { os.write(b); + } catch (IOException e) { + throw new NativeLibraryToolException("Failed to write temporary PE library copy '" + tempfile + "' for IsolateNativeModules relocation: " + e.getMessage(), e); } } @Override - public void setId(String newId) throws IOException { + public void setId(String newId) { // TODO } - private String getDelvewheelPython() { + private String getDelvewheelPython() throws NativeLibraryToolException { TruffleFile delvewheel = which(context, "delvewheel.exe"); if (!delvewheel.exists()) { delvewheel = which(context, "delvewheel.bat"); @@ -73,6 +87,9 @@ private String getDelvewheelPython() { if (!delvewheel.exists()) { delvewheel = which(context, "delvewheel.cmd"); } + if (!delvewheel.exists()) { + throw new NativeLibraryToolException("Could not find `delvewheel`. " + DELVEWHEEL_INSTALL_INSTRUCTION); + } TruffleFile python = delvewheel.resolveSibling("python.exe"); if (!python.exists()) { python = delvewheel.resolveSibling("python.bat"); @@ -89,30 +106,59 @@ private String getDelvewheelPython() { if (!python.exists()) { python = delvewheel.getParent().resolveSibling("python.cmd"); } + if (!python.exists()) { + throw new NativeLibraryToolException("Could not find Python executable next to `delvewheel` at '" + delvewheel + "'. " + DELVEWHEEL_INSTALL_INSTRUCTION); + } return python.toString(); } @Override - public void changeOrAddDependency(String oldName, String newName) throws IOException, InterruptedException { + public void changeOrAddDependency(String oldName, String newName) throws NativeLibraryToolException { var pb = newProcessBuilder(context); + var stderr = new ByteArrayOutputStream(); + pb.redirectError(pb.createRedirectToStream(stderr)); var tempfileWithForwardSlashes = tempfile.toString().replace('\\', '/'); String pythonExe = getDelvewheelPython(); pb.command(pythonExe, "-c", String.format("from delvewheel import _dll_utils; _dll_utils.replace_needed('%s', ['%s'], {'%s': '%s'}, strip=True, verbose=2, test=[])", tempfileWithForwardSlashes, oldName, oldName, newName)); - var proc = pb.start(); - if (proc.waitFor() != 0) { - throw new IOException("Failed to run `delvewheel` 1.9.0 to copy required DLL. Make sure you have it installed in your venv."); + Process proc; + try { + proc = pb.start(); + } catch (IOException e) { + throw new NativeLibraryToolException("Failed to start `delvewheel` to copy required DLL: " + e.getMessage() + ". " + DELVEWHEEL_INSTALL_INSTRUCTION, e); } + try { + if (proc.waitFor() != 0) { + throw new NativeLibraryToolException("Failed to run `delvewheel` to copy required DLL (exit code " + proc.exitValue() + "). " + DELVEWHEEL_INSTALL_INSTRUCTION + + " Stderr: " + getStderr(stderr)); + } + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + throw new NativeLibraryToolException("Interrupted while waiting for `delvewheel` to copy required DLL. " + DELVEWHEEL_INSTALL_INSTRUCTION, e); + } + } + + private static String getStderr(ByteArrayOutputStream stderr) { + String output = stderr.toString(StandardCharsets.UTF_8).strip(); + return output.isEmpty() ? "" : output; } @Override - public void write(TruffleFile copy) throws IOException { - tempfile.copy(copy, StandardCopyOption.REPLACE_EXISTING, StandardCopyOption.COPY_ATTRIBUTES); + 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 PE library copy '" + copy + "': " + e.getMessage(), e); + } } @Override - public void close() throws IOException { - tempfile.delete(); + public void close() throws NativeLibraryToolException { + try { + tempfile.delete(); + } catch (IOException e) { + throw new NativeLibraryToolException("Failed to delete temporary PE library copy '" + tempfile + "': " + e.getMessage(), e); + } } } diff --git a/graalpython/com.oracle.graal.python/src/com/oracle/graal/python/builtins/objects/cext/copying/SharedObject.java b/graalpython/com.oracle.graal.python/src/com/oracle/graal/python/builtins/objects/cext/copying/SharedObject.java index 0fd42baffd..2876aad951 100644 --- a/graalpython/com.oracle.graal.python/src/com/oracle/graal/python/builtins/objects/cext/copying/SharedObject.java +++ b/graalpython/com.oracle.graal.python/src/com/oracle/graal/python/builtins/objects/cext/copying/SharedObject.java @@ -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 @@ -51,25 +51,30 @@ import com.oracle.truffle.api.io.TruffleProcessBuilder; abstract class SharedObject implements AutoCloseable { - abstract void setId(String newId) throws IOException, InterruptedException; + abstract void setId(String newId) throws NativeLibraryToolException; - abstract void changeOrAddDependency(String oldName, String newName) throws IOException, InterruptedException; + abstract void changeOrAddDependency(String oldName, String newName) throws NativeLibraryToolException; - abstract void write(TruffleFile copy) throws IOException, InterruptedException; + abstract void write(TruffleFile copy) throws NativeLibraryToolException; - public abstract void close() throws IOException; + public abstract void close() throws NativeLibraryToolException; - static SharedObject open(TruffleFile file, PythonContext context) throws IOException { - var f = file.readAllBytes(); + static SharedObject open(TruffleFile file, PythonContext context) throws NativeLibraryToolException { + byte[] f; + try { + f = file.readAllBytes(); + } catch (IOException e) { + throw new NativeLibraryToolException("Failed to read native library '" + file + "' for IsolateNativeModules relocation: " + e.getMessage(), e); + } switch (f[0]) { case 0x7f: return new ElfFile(f, context); case 0x4d, 0x5a: return new PEFile(f, context); case (byte) 0xca, (byte) 0xfe, (byte) 0xce, (byte) 0xcf: - throw new IOException("Modifying Mach-O files is not yet supported"); + throw new NativeLibraryToolException("Failed to relocate native library '" + file + "': modifying Mach-O files is not yet supported."); default: - throw new IOException("Unknown shared object format"); + throw new NativeLibraryToolException("Failed to relocate native library '" + file + "': unknown shared object format."); } }