Skip to content

Commit 95b9693

Browse files
committed
add support for external resources dir to JBang integration
1 parent d0e4ab9 commit 95b9693

File tree

1 file changed

+80
-123
lines changed

1 file changed

+80
-123
lines changed

graalpython/org.graalvm.python.jbang/src/org/graalvm/python/jbang/JBangIntegration.java

Lines changed: 80 additions & 123 deletions
Original file line numberDiff line numberDiff line change
@@ -41,17 +41,14 @@
4141

4242
package org.graalvm.python.jbang;
4343

44-
import org.graalvm.python.embedding.tools.exec.GraalPyRunner;
4544
import org.graalvm.python.embedding.tools.exec.SubprocessLog;
4645
import org.graalvm.python.embedding.tools.vfs.VFSUtils;
4746

4847
import java.io.File;
49-
import java.io.FileWriter;
5048
import java.io.IOException;
5149
import java.nio.file.Files;
5250
import java.nio.file.Path;
5351
import java.nio.file.Paths;
54-
import java.nio.file.attribute.PosixFilePermission;
5552
import java.util.ArrayList;
5653
import java.util.Arrays;
5754
import java.util.Collection;
@@ -71,6 +68,7 @@
7168
public class JBangIntegration {
7269
private static final String PIP = "//PIP";
7370
private static final String PIP_DROP = "//PIP_DROP";
71+
private static final String RESOURCES_DIRECTORY = "//PYTHON_RESOURCES_DIRECTORY";
7472
private static final String PYTHON_LANGUAGE = "python-language";
7573
private static final String PYTHON_RESOURCES = "python-resources";
7674
private static final String PYTHON_LAUNCHER = "python-launcher";
@@ -80,6 +78,7 @@ public class JBangIntegration {
8078

8179
private static final SubprocessLog LOG = new SubprocessLog() {
8280
};
81+
private static final String JBANG_COORDINATES = "org.graalvm.python:graalpy-jbang:jar";
8382

8483
/**
8584
*
@@ -99,29 +98,86 @@ public static Map<String, Object> postBuild(Path temporaryJar,
9998
List<Map.Entry<String, String>> repositories,
10099
List<Map.Entry<String, Path>> dependencies,
101100
List<String> comments,
102-
boolean nativeImage) {
103-
Path vfs = temporaryJar.resolve(VFS_ROOT);
104-
Path venv = vfs.resolve(VFS_VENV);
105-
Path home = vfs.resolve(VFS_HOME);
101+
boolean nativeImage) throws IOException {
106102

107-
try {
103+
Path resourcesDirectory = null;
104+
List<String> pkgs = new ArrayList<>();
105+
boolean seenResourceDir = false;
106+
for (String comment : comments) {
107+
if (comment.startsWith(RESOURCES_DIRECTORY)) {
108+
if (seenResourceDir) {
109+
throw new IllegalStateException("only one " + RESOURCES_DIRECTORY + " comment is allowed");
110+
}
111+
seenResourceDir = true;
112+
String path = comment.substring(RESOURCES_DIRECTORY.length()).trim();
113+
if (!path.isEmpty()) {
114+
resourcesDirectory = Path.of(path);
115+
}
116+
} else if (comment.startsWith(PIP)) {
117+
pkgs.addAll(Arrays.stream(comment.substring(PIP.length()).trim().split(" ")).filter(s -> !s.trim().isEmpty()).collect(Collectors.toList()));
118+
}
119+
}
120+
if (!pkgs.isEmpty()) {
121+
log("python packages: " + pkgs);
122+
}
123+
124+
Path vfs = null;
125+
Path venv;
126+
Path home;
127+
if (resourcesDirectory == null) {
128+
vfs = temporaryJar.resolve(VFS_ROOT);
108129
Files.createDirectories(vfs);
109-
} catch (IOException e) {
110-
throw new Error(e);
130+
venv = vfs.resolve(VFS_VENV);
131+
home = vfs.resolve(VFS_HOME);
132+
} else {
133+
log("python resources directory: " + resourcesDirectory);
134+
venv = resourcesDirectory.resolve(VFS_VENV);
135+
home = resourcesDirectory.resolve(VFS_HOME);
111136
}
112137

113-
for (String comment : comments) {
114-
if (comment.startsWith(PIP)) {
115-
ensureVenv(venv, dependencies);
116-
try {
117-
String[] pkgs = Arrays.stream(comment.substring(PIP.length()).trim().split(" ")).filter(s -> !s.trim().isEmpty()).toArray(String[]::new);
118-
GraalPyRunner.runPip(venv, "install", LOG, pkgs);
119-
} catch (IOException | InterruptedException e) {
120-
throw new RuntimeException(e);
121-
}
138+
if (resourcesDirectory != null || !pkgs.isEmpty()) {
139+
handleVenv(venv, dependencies, pkgs, comments, resourcesDirectory == null);
140+
}
141+
142+
if (nativeImage) {
143+
// include python stdlib in image
144+
try {
145+
VFSUtils.copyGraalPyHome(calculateClasspath(dependencies), home, null, null, LOG);
146+
VFSUtils.writeNativeImageConfig(temporaryJar.resolve("META-INF"), "graalpy-jbang-integration");
147+
} catch (IOException | InterruptedException e) {
148+
throw new RuntimeException(e);
122149
}
123150
}
124-
if (Files.exists(venv)) {
151+
152+
if (vfs != null) {
153+
try {
154+
VFSUtils.generateVFSFilesList(vfs);
155+
} catch (IOException e) {
156+
throw new RuntimeException(e);
157+
}
158+
}
159+
return new HashMap<>();
160+
}
161+
162+
private static Path getLauncherPath(String projectPath) {
163+
return Paths.get(projectPath, LAUNCHER).toAbsolutePath();
164+
}
165+
166+
private static void handleVenv(Path venv, List<Map.Entry<String, Path>> dependencies, List<String> pkgs, List<String> comments, boolean dropPip) throws IOException {
167+
String graalPyVersion = dependencies.stream().filter((e) -> e.getKey().startsWith(JBANG_COORDINATES)).map(e -> e.getKey().substring(JBANG_COORDINATES.length() + 1)).findFirst().orElseGet(
168+
null);
169+
if (graalPyVersion == null) {
170+
// perhaps already checked by jbang
171+
throw new IllegalStateException("could not resolve GraalPy version from provided dependencies");
172+
}
173+
Path venvParent = venv.getParent();
174+
if (venvParent == null) {
175+
// perhaps already checked by jbang
176+
throw new IllegalStateException("could not resolve parent for venv path: " + venv);
177+
}
178+
VFSUtils.createVenv(venv, pkgs, getLauncherPath(venvParent.toString()), () -> calculateClasspath(dependencies), graalPyVersion, LOG, (txt) -> LOG.log(txt));
179+
180+
if (dropPip) {
125181
try {
126182
Stream<Path> filter = Files.list(venv.resolve("lib")).filter(p -> p.getFileName().toString().startsWith("python3"));
127183
// on windows, there doesn't have to be python3xxxx folder.
@@ -149,109 +205,6 @@ public static Map<String, Object> postBuild(Path temporaryJar,
149205
throw new RuntimeException(e);
150206
}
151207
}
152-
153-
if (nativeImage) {
154-
// include python stdlib in image
155-
try {
156-
VFSUtils.copyGraalPyHome(calculateClasspath(dependencies), home, null, null, LOG);
157-
VFSUtils.writeNativeImageConfig(temporaryJar.resolve("META-INF"), "graalpy-jbang-integration");
158-
} catch (IOException | InterruptedException e) {
159-
throw new RuntimeException(e);
160-
}
161-
}
162-
163-
try {
164-
VFSUtils.generateVFSFilesList(vfs);
165-
} catch (IOException e) {
166-
throw new RuntimeException(e);
167-
}
168-
return new HashMap<>();
169-
}
170-
171-
private static Path getLauncherPath(String projectPath) {
172-
return Paths.get(projectPath, LAUNCHER);
173-
}
174-
175-
private static void generateLaunchers(List<Map.Entry<String, Path>> dependencies, String projectPath) {
176-
System.out.println("Generating GraalPy launchers");
177-
var launcher = getLauncherPath(projectPath);
178-
if (!Files.exists(launcher)) {
179-
var classpath = calculateClasspath(dependencies);
180-
var java = Paths.get(System.getProperty("java.home"), "bin", "java");
181-
if (!IS_WINDOWS) {
182-
var script = String.format("""
183-
#!/usr/bin/env bash
184-
%s -classpath %s %s --python.Executable="$0" "$@"
185-
""",
186-
java,
187-
String.join(File.pathSeparator, classpath),
188-
"com.oracle.graal.python.shell.GraalPythonMain");
189-
try {
190-
Path parent = launcher.getParent();
191-
if (parent != null) {
192-
Files.createDirectories(parent);
193-
}
194-
Files.writeString(launcher, script);
195-
var perms = Files.getPosixFilePermissions(launcher);
196-
perms.addAll(List.of(PosixFilePermission.OWNER_EXECUTE, PosixFilePermission.GROUP_EXECUTE, PosixFilePermission.OTHERS_EXECUTE));
197-
Files.setPosixFilePermissions(launcher, perms);
198-
} catch (IOException e) {
199-
throw new RuntimeException(e);
200-
}
201-
} else {
202-
// on windows, generate a venv launcher
203-
var script = String.format("""
204-
import os, shutil, struct, venv
205-
from pathlib import Path
206-
vl = os.path.join(venv.__path__[0], 'scripts', 'nt', 'graalpy.exe')
207-
tl = os.path.join(r'%s')
208-
os.makedirs(Path(tl).parent.absolute(), exist_ok=True)
209-
shutil.copy(vl, tl)
210-
cmd = r'%s -classpath "%s" %s'
211-
pyvenvcfg = os.path.join(os.path.dirname(tl), "pyvenv.cfg")
212-
with open(pyvenvcfg, 'w', encoding='utf-8') as f:
213-
f.write('venvlauncher_command = ')
214-
f.write(cmd)
215-
""",
216-
launcher,
217-
java,
218-
String.join(File.pathSeparator, classpath),
219-
"com.oracle.graal.python.shell.GraalPythonMain");
220-
File tmp;
221-
try {
222-
tmp = File.createTempFile("create_launcher", ".py");
223-
tmp.deleteOnExit();
224-
try (var wr = new FileWriter(tmp)) {
225-
wr.write(script);
226-
}
227-
GraalPyRunner.run(calculateClasspath(dependencies), LOG, tmp.getAbsolutePath());
228-
} catch (IOException | InterruptedException e) {
229-
throw new RuntimeException(e);
230-
}
231-
}
232-
}
233-
}
234-
235-
private static void ensureVenv(Path venv, List<Map.Entry<String, Path>> dependencies) {
236-
if (Files.exists(venv)) {
237-
return;
238-
}
239-
Path venvDirectory = venv.toAbsolutePath();
240-
Path parent = venv.getParent();
241-
if (parent != null) {
242-
String parentString = parent.toString();
243-
generateLaunchers(dependencies, parentString);
244-
try {
245-
GraalPyRunner.runLauncher(getLauncherPath(parentString).toString(), LOG, "-m", "venv", venvDirectory.toString(), "--without-pip");
246-
} catch (IOException | InterruptedException e) {
247-
throw new RuntimeException(e);
248-
}
249-
try {
250-
GraalPyRunner.runVenvBin(venvDirectory, "graalpy", LOG, "-I", "-m", "ensurepip");
251-
} catch (IOException | InterruptedException e) {
252-
throw new RuntimeException(e);
253-
}
254-
}
255208
}
256209

257210
private static Collection<Path> resolveProjectDependencies(List<Map.Entry<String, Path>> dependencies) {
@@ -284,4 +237,8 @@ private static HashSet<String> calculateClasspath(List<Map.Entry<String, Path>>
284237
}
285238
return classpath;
286239
}
240+
241+
private static void log(String txt) {
242+
LOG.log("[graalpy jbang integration] " + txt);
243+
}
287244
}

0 commit comments

Comments
 (0)