Skip to content

Commit f361f88

Browse files
committed
Support for Windows in standalone module
1 parent a52b465 commit f361f88

File tree

4 files changed

+131
-54
lines changed

4 files changed

+131
-54
lines changed

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

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -971,7 +971,11 @@ Object list(TruffleString dirPath, TruffleString filesListPath) {
971971
List<String> ret = list(dir);
972972
String parentPathString = dir.getParent().getAbsoluteFile().getPath();
973973
for (String f : ret) {
974-
bw.write(f.substring(parentPathString.length()));
974+
String tt = f.substring(parentPathString.length());
975+
if (tt.charAt(0) == '\\') {
976+
tt = tt.replace("\\", "/");
977+
}
978+
bw.write(tt);
975979
bw.write("\n");
976980
}
977981
} catch (IOException e) {

graalpython/lib-graalpython/modules/standalone/templates/Main.java

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -57,11 +57,12 @@ public class Main {
5757
private static String PYTHON = "python";
5858

5959
public static void main(String[] args) throws IOException {
60+
VirtualFileSystem vfs = new VirtualFileSystem();
6061
Builder builder = Context.newBuilder()
6162
.allowExperimentalOptions(true)
6263
.allowAllAccess(true)
6364
.allowIO(true)
64-
.fileSystem(new VirtualFileSystem())
65+
.fileSystem(vfs)
6566
.option("python.PosixModuleBackend", "java")
6667
.option("python.DontWriteBytecodeFlag", "true")
6768
.option("python.VerboseFlag", System.getenv("PYTHONVERBOSE") != null ? "true" : "false")
@@ -70,12 +71,12 @@ public static void main(String[] args) throws IOException {
7071
.option("python.AlwaysRunExcepthook", "false")
7172
.option("python.ForceImportSite", "true")
7273
.option("python.RunViaLauncher", "false")
73-
.option("python.Executable", VENV_PREFIX + "/bin/python")
74-
.option("python.InputFilePath", PROJ_PREFIX)
75-
.option("python.CheckHashPycsMode", "never");
74+
.option("python.Executable", vfs.resourcePathToPlatformPath(VENV_PREFIX) + (VirtualFileSystem.isWindows() ? "\\Scripts\\python.cmd" : "/bin/python"))
75+
.option("python.InputFilePath", vfs.resourcePathToPlatformPath(PROJ_PREFIX))
76+
.option("python.CheckHashPycsMode", "never")
77+
.option("engine.WarnInterpreterOnly", "false");
7678
if(ImageInfo.inImageRuntimeCode()) {
77-
builder.option("engine.WarnInterpreterOnly", "false")
78-
.option("python.PythonHome", HOME_PREFIX);
79+
builder.option("python.PythonHome", vfs.resourcePathToPlatformPath(HOME_PREFIX));
7980
}
8081
Context context = builder.build();
8182

@@ -99,6 +100,5 @@ public static void main(String[] args) throws IOException {
99100
}
100101
}
101102
}
102-
103-
103+
104104
}

graalpython/lib-graalpython/modules/standalone/templates/VirtualFileSystem.java

Lines changed: 110 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,7 @@
6565
import java.util.HashSet;
6666
import java.util.Iterator;
6767
import java.util.List;
68+
import java.util.Locale;
6869
import java.util.Map;
6970
import java.util.Set;
7071
import java.util.TreeMap;
@@ -73,66 +74,143 @@
7374
public final class VirtualFileSystem implements FileSystem {
7475

7576
/*
76-
* Virtual filesystem root
77+
* Root of the virtual filesystem in the resources.
7778
*/
78-
static final String VFS_PREFIX = "/{vfs-prefix}";
79+
private static final String VFS_PREFIX = "/vfs";
7980

8081
/*
81-
* Index of all files and directories available in the filessytem at runtime.
82-
* - paths are relative to the filesystem root
82+
* Index of all files and directories available in the resources at runtime.
83+
* - paths are absolute
8384
* - directory paths end with a '/'
84-
* Used to determine directy entries, if an entry is a file or a directory, etc.
85+
* - uses '/' separator regardless of platform.
86+
* Used to determine directory entries, if an entry is a file or a directory, etc.
8587
*/
8688
private static final String FILES_LIST_PATH = VFS_PREFIX + "/{files-list-name}";
8789

88-
private static final TreeMap<Path, Entry> VFS_ENTRIES = new TreeMap<>();
90+
/*
91+
* Maps platform-specific paths to entries.
92+
*/
93+
private static final TreeMap<String, Entry> VFS_ENTRIES = new TreeMap<>();
8994

95+
/*
96+
* These use '/' as the separator and start with VFS_PREFIX, no trailing slashes.
97+
*/
9098
private static Set<String> filesList;
9199
private static Set<String> dirsList;
100+
private static Map<String, String> lowercaseToResourceMap;
92101

93102
private final FileSystem delegate = FileSystem.newDefaultFileSystem();
94103

95-
static final record Entry(boolean isFile, Object data) {};
104+
private static final String PLATFORM_SEPARATOR = Paths.get("").getFileSystem().getSeparator();
105+
private static final char RESOURCE_SEPARATOR_CHAR = '/';
106+
private static final String RESOURCE_SEPARATOR = String.valueOf(RESOURCE_SEPARATOR_CHAR);
107+
108+
/*
109+
* For files, `data` is a byte[], for directories it is a Path[] which
110+
* contains platform-specific paths.
111+
*/
112+
private static final record Entry(boolean isFile, Object data) {};
113+
114+
/*
115+
* Determines where the virtual filesystem lives in the real filesystem,
116+
* e.g. if set to "X:\graalpy_vfs", then a resource with path /vfx/xyz/abc
117+
* is visible as "X:\graalpy_vfs\xyz\abc". This needs to be an absolute path
118+
* with platform-specific separators without any trailing separator.
119+
* If that file or directory actually exists, it will not be accessible.
120+
*/
121+
private final String mountPoint;
122+
private static final boolean caseInsensitive = isWindows();
96123

97-
private static void putVFSEntry(Path p, Entry e) throws IOException {
98-
VFS_ENTRIES.put(toRealPathStatic(toAbsolutePathStatic(p)), e);
124+
public VirtualFileSystem() {
125+
String mp = System.getenv("GRAALPY_VFS_MOUNT_POINT");
126+
if (mp == null) {
127+
mp = isWindows() ? "X:\\graalpy_vfs" : "/graalpy_vfs";
128+
}
129+
if (mp.endsWith(PLATFORM_SEPARATOR) || !Path.of(mp).isAbsolute()) {
130+
throw new IllegalArgumentException("GRAALPY_VFS_MOUNT_POINT must be set to an absolute path without a trailing separator");
131+
}
132+
this.mountPoint = mp;
99133
}
100134

135+
public static boolean isWindows() {
136+
return System.getProperty("os.name").toLowerCase(Locale.ROOT).contains("windows");
137+
}
138+
139+
public String resourcePathToPlatformPath(String path) {
140+
assert path.startsWith(VFS_PREFIX);
141+
path = path.substring(VFS_PREFIX.length());
142+
if (!PLATFORM_SEPARATOR.equals(RESOURCE_SEPARATOR)) {
143+
path = path.replace(RESOURCE_SEPARATOR, PLATFORM_SEPARATOR);
144+
}
145+
return mountPoint + path;
146+
}
147+
148+
private String platformPathToResourcePath(String path) throws IOException {
149+
assert path.startsWith(mountPoint);
150+
151+
path = path.substring(mountPoint.length());
152+
if (!PLATFORM_SEPARATOR.equals(RESOURCE_SEPARATOR)) {
153+
path = path.replace(PLATFORM_SEPARATOR, RESOURCE_SEPARATOR);
154+
}
155+
if (path.endsWith(RESOURCE_SEPARATOR)) {
156+
path = path.substring(0, path.length() - RESOURCE_SEPARATOR.length());
157+
}
158+
path = VFS_PREFIX + path;
159+
if (caseInsensitive) {
160+
path = getLowercaseToResourceMap().get(path);
161+
}
162+
return path;
163+
}
164+
101165
private static Set<String> getFilesList() throws IOException {
102-
if(filesList == null) {
166+
if (filesList == null) {
103167
initFilesAndDirsList();
104168
}
105169
return filesList;
106170
}
107171

108172
private static Set<String> getDirsList() throws IOException {
109-
if(dirsList == null) {
173+
if (dirsList == null) {
110174
initFilesAndDirsList();
111175
}
112176
return dirsList;
113-
}
177+
}
178+
179+
private static Map<String, String> getLowercaseToResourceMap() throws IOException {
180+
assert caseInsensitive;
181+
if (lowercaseToResourceMap == null) {
182+
initFilesAndDirsList();
183+
}
184+
return lowercaseToResourceMap;
185+
}
114186

115187
private static void initFilesAndDirsList() throws IOException {
116188
filesList = new HashSet<>();
117189
dirsList = new HashSet<>();
190+
if (caseInsensitive) {
191+
lowercaseToResourceMap = new HashMap<>();
192+
}
118193
try(InputStream stream = VirtualFileSystem.class.getResourceAsStream(FILES_LIST_PATH)) {
119194
if (stream == null) {
120195
return;
121196
}
122197
BufferedReader br = new BufferedReader(new InputStreamReader(stream));
123198
String line;
124199
while((line = br.readLine()) != null) {
125-
if(line.endsWith("/")) {
200+
if(line.endsWith(RESOURCE_SEPARATOR)) {
126201
line = line.substring(0, line.length() - 1);
127202
dirsList.add(line);
128203
} else {
129204
filesList.add(line);
130-
}
205+
}
206+
if (caseInsensitive) {
207+
lowercaseToResourceMap.put(line.toLowerCase(Locale.ROOT), line);
208+
}
131209
}
132210
}
133211
}
134212

135-
private static Entry readDirEntry(String parentDir) throws IOException {
213+
private Entry readDirEntry(String parentDir) throws IOException {
136214
List<String> l = new ArrayList<>();
137215

138216
// find all files in parent dir
@@ -151,14 +229,14 @@ private static Entry readDirEntry(String parentDir) throws IOException {
151229

152230
Path[] paths = new Path[l.size()];
153231
for (int i = 0; i < paths.length; i++) {
154-
paths[i] = Paths.get(l.get(i));
232+
paths[i] = Paths.get(resourcePathToPlatformPath(l.get(i)));
155233
}
156234
return new Entry(false, paths);
157235
}
158236

159237
private static boolean isParent(String parentDir, String file) {
160238
return file.length() > parentDir.length() && file.startsWith(parentDir) &&
161-
file.indexOf("/", parentDir.length() + 1) < 0;
239+
file.indexOf(RESOURCE_SEPARATOR_CHAR, parentDir.length() + 1) < 0;
162240
}
163241

164242
private static Entry readFileEntry(String file) throws IOException {
@@ -183,22 +261,20 @@ static byte[] readResource(String path) throws IOException {
183261
}
184262

185263
private Entry file(Path path) throws IOException {
186-
Entry e = VFS_ENTRIES.get(toRealPath(toAbsolutePath(path)));
264+
path = toRealPath(toAbsolutePath(path));
265+
String pathString = path.toString();
266+
String entryKey = caseInsensitive ? pathString.toLowerCase(Locale.ROOT) : pathString;
267+
Entry e = VFS_ENTRIES.get(entryKey);
187268
if(e == null) {
188-
String pathString = path.toString();
189-
if(pathString.endsWith("/")) {
190-
pathString = pathString.substring(0, pathString.length() - 1);
191-
}
192-
193-
URL uri = VirtualFileSystem.class.getResource(pathString);
269+
pathString = platformPathToResourcePath(pathString);
270+
URL uri = pathString == null ? null : VirtualFileSystem.class.getResource(pathString);
194271
if(uri != null) {
195272
if(getDirsList().contains(pathString)) {
196273
e = readDirEntry(pathString);
197274
} else {
198275
e = readFileEntry(pathString);
199-
getFilesList().remove(pathString);
200276
}
201-
putVFSEntry(path, e);
277+
VFS_ENTRIES.put(entryKey, e);
202278
}
203279
}
204280
return e;
@@ -220,7 +296,7 @@ public Path parsePath(String path) {
220296

221297
@Override
222298
public void checkAccess(Path path, Set<? extends AccessMode> modes, LinkOption... linkOptions) throws IOException {
223-
if (path.normalize().startsWith(VFS_PREFIX)) {
299+
if (path.normalize().startsWith(mountPoint)) {
224300
if (modes.contains(AccessMode.WRITE)) {
225301
throw new IOException("read-only filesystem");
226302
}
@@ -234,7 +310,7 @@ public void checkAccess(Path path, Set<? extends AccessMode> modes, LinkOption..
234310

235311
@Override
236312
public void createDirectory(Path dir, FileAttribute<?>... attrs) throws IOException {
237-
if (dir.normalize().startsWith(VFS_PREFIX)) {
313+
if (dir.normalize().startsWith(mountPoint)) {
238314
throw new SecurityException("read-only filesystem");
239315
} else {
240316
delegate.createDirectory(dir, attrs);
@@ -243,7 +319,7 @@ public void createDirectory(Path dir, FileAttribute<?>... attrs) throws IOExcept
243319

244320
@Override
245321
public void delete(Path path) throws IOException {
246-
if (path.normalize().startsWith(VFS_PREFIX)) {
322+
if (path.normalize().startsWith(mountPoint)) {
247323
throw new SecurityException("read-only filesystem");
248324
} else {
249325
delegate.delete(path);
@@ -252,7 +328,7 @@ public void delete(Path path) throws IOException {
252328

253329
@Override
254330
public SeekableByteChannel newByteChannel(Path path, Set<? extends OpenOption> options, FileAttribute<?>... attrs) throws IOException {
255-
if (!path.normalize().startsWith(VFS_PREFIX)) {
331+
if (!path.normalize().startsWith(mountPoint)) {
256332
return delegate.newByteChannel(path, options, attrs);
257333
}
258334

@@ -328,7 +404,7 @@ public void close() throws IOException {
328404

329405
@Override
330406
public DirectoryStream<Path> newDirectoryStream(Path dir, DirectoryStream.Filter<? super Path> filter) throws IOException {
331-
if (!dir.normalize().startsWith(VFS_PREFIX)) {
407+
if (!dir.normalize().startsWith(mountPoint)) {
332408
return delegate.newDirectoryStream(dir, filter);
333409
}
334410
Entry e = file(dir);
@@ -354,29 +430,21 @@ public Iterator<Path> iterator() {
354430

355431
@Override
356432
public Path toAbsolutePath(Path path) {
357-
return toAbsolutePathStatic(path);
358-
}
359-
360-
private static Path toAbsolutePathStatic(Path path) {
361-
if (path.startsWith("/")) {
433+
if (path.startsWith(mountPoint)) {
362434
return path;
363435
} else {
364-
return Paths.get("/", path.toString());
436+
return Paths.get(mountPoint, path.toString());
365437
}
366438
}
367439

368440
@Override
369441
public Path toRealPath(Path path, LinkOption... linkOptions) throws IOException {
370-
return toRealPathStatic(path, linkOptions);
371-
}
372-
373-
private static Path toRealPathStatic(Path path, LinkOption... linkOptions) throws IOException {
374442
return path.normalize();
375443
}
376444

377445
@Override
378446
public Map<String, Object> readAttributes(Path path, String attributes, LinkOption... options) throws IOException {
379-
if (!path.normalize().startsWith(VFS_PREFIX)) {
447+
if (!path.normalize().startsWith(mountPoint)) {
380448
return delegate.readAttributes(path, attributes, options);
381449
}
382450
Entry e = file(path);

graalpython/lib-graalpython/modules/standalone/templates/polyglot_app_pom.xml

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,7 @@
5353
<graal.sdk.version>{graal-sdk-version}</graal.sdk.version>
5454
<native.image.maven.plugin.version>{native-image-mvn-plugin}</native.image.maven.plugin.version>
5555
<graalpy.executable>${env.JAVA_HOME}/bin/graalpy</graalpy.executable>
56+
<graalpy.venv.executable>/bin/python</graalpy.venv.executable>
5657
</properties>
5758

5859
<build>
@@ -84,7 +85,7 @@
8485
<executable>${graalpy.executable}</executable>
8586
<arguments>
8687
<argument>-c</argument>
87-
<argument>__graalpython__.list_files('${project.basedir}/src/main/resources/{vfs-prefix}', '${project.build.directory}/classes/{vfs-prefix}/{files-list-name}')</argument>
88+
<argument>__graalpython__.list_files(r'${project.basedir}/src/main/resources/{vfs-prefix}', r'${project.build.directory}/classes/{vfs-prefix}/{files-list-name}')</argument>
8889
</arguments>
8990
</configuration>
9091
</execution>
@@ -192,6 +193,7 @@
192193
<profile>
193194
<properties>
194195
<graalpy.executable>${env.JAVA_HOME}/bin/graalpy.cmd</graalpy.executable>
196+
<graalpy.venv.executable>/Scripts/python.cmd</graalpy.venv.executable>
195197
</properties>
196198
<activation>
197199
<os>
@@ -232,8 +234,11 @@
232234
<goal>exec</goal>
233235
</goals>
234236
<configuration>
235-
<executable>${project.basedir}/src/main/resources/{vfs-venv-prefix}/bin/pip</executable>
237+
<executable>${project.basedir}/src/main/resources/{vfs-venv-prefix}${graalpy.venv.executable}</executable>
236238
<arguments>
239+
<argument>-m</argument>
240+
<argument>pip</argument>
241+
<argument>--no-cache-dir</argument>
237242
<argument>install</argument>
238243
<argument>termcolor</argument>
239244
</arguments>
@@ -248,7 +253,7 @@
248253
</build>
249254
<activation>
250255
<file>
251-
<missing>src/main/resources/{vfs-venv-prefix}/bin/pip</missing>
256+
<missing>src/main/resources/vfs/venv/pyvenv.cfg</missing>
252257
</file>
253258
</activation>
254259
</profile>

0 commit comments

Comments
 (0)