41
41
42
42
package org .graalvm .python .jbang ;
43
43
44
- import org .graalvm .python .embedding .tools .exec .GraalPyRunner ;
45
44
import org .graalvm .python .embedding .tools .exec .SubprocessLog ;
46
45
import org .graalvm .python .embedding .tools .vfs .VFSUtils ;
47
46
48
47
import java .io .File ;
49
- import java .io .FileWriter ;
50
48
import java .io .IOException ;
51
49
import java .nio .file .Files ;
52
50
import java .nio .file .Path ;
53
51
import java .nio .file .Paths ;
54
- import java .nio .file .attribute .PosixFilePermission ;
55
52
import java .util .ArrayList ;
56
53
import java .util .Arrays ;
57
54
import java .util .Collection ;
71
68
public class JBangIntegration {
72
69
private static final String PIP = "//PIP" ;
73
70
private static final String PIP_DROP = "//PIP_DROP" ;
71
+ private static final String RESOURCES_DIRECTORY = "//PYTHON_RESOURCES_DIRECTORY" ;
74
72
private static final String PYTHON_LANGUAGE = "python-language" ;
75
73
private static final String PYTHON_RESOURCES = "python-resources" ;
76
74
private static final String PYTHON_LAUNCHER = "python-launcher" ;
@@ -80,6 +78,7 @@ public class JBangIntegration {
80
78
81
79
private static final SubprocessLog LOG = new SubprocessLog () {
82
80
};
81
+ private static final String JBANG_COORDINATES = "org.graalvm.python:graalpy-jbang:jar" ;
83
82
84
83
/**
85
84
*
@@ -99,29 +98,86 @@ public static Map<String, Object> postBuild(Path temporaryJar,
99
98
List <Map .Entry <String , String >> repositories ,
100
99
List <Map .Entry <String , Path >> dependencies ,
101
100
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 {
106
102
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 );
108
129
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 );
111
136
}
112
137
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 );
122
149
}
123
150
}
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 ) {
125
181
try {
126
182
Stream <Path > filter = Files .list (venv .resolve ("lib" )).filter (p -> p .getFileName ().toString ().startsWith ("python3" ));
127
183
// on windows, there doesn't have to be python3xxxx folder.
@@ -149,109 +205,6 @@ public static Map<String, Object> postBuild(Path temporaryJar,
149
205
throw new RuntimeException (e );
150
206
}
151
207
}
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
- }
255
208
}
256
209
257
210
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>>
284
237
}
285
238
return classpath ;
286
239
}
240
+
241
+ private static void log (String txt ) {
242
+ LOG .log ("[graalpy jbang integration] " + txt );
243
+ }
287
244
}
0 commit comments