Skip to content

Commit 4efdcb2

Browse files
melixsteve-s
authored andcommitted
[GR-59841] Polish GraalPy Gradle plugin.
PullRequest: graalpython/3488
2 parents cc99233 + 1fd4657 commit 4efdcb2

File tree

18 files changed

+1005
-466
lines changed

18 files changed

+1005
-466
lines changed

ci.jsonnet

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
{ "overlay": "6cf9b65e57d85fa77c95fa3451bb11ef674bde97" }
1+
{ "overlay": "cead6ebaabd0279780046d2e1826395d9ce8bdc4" }

docs/user/Embedding-Build-Tools.md

Lines changed: 26 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -117,19 +117,30 @@ Remember to use the appropriate `GraalPyResources` API to create the Context.
117117

118118
## GraalPy Gradle Plugin Configuration
119119

120-
> Note: GraalPy Gradle Plugin will become available as of GraalPy version 24.1.1, planned for October 15, 2024.
120+
The plugin must be added to the plugins section in the _build.gradle_ file.
121+
The **version** property defines which version of GraalPy to use.
122+
```
123+
plugins {
124+
// other plugins ...
125+
id 'org.graalvm.python' version '24.2.0'
126+
}
127+
```
121128

122-
Add the plugin configuration in the `GraalPy` block in the _build.gradle_ file.
123-
The **packages** element declares a list of third-party Python packages to be downloaded and installed by the plugin.
124-
- The Python packages and their versions are specified as if used with `pip`.
129+
The plugin automatically injects these dependencies of the same version as the plugin version:
130+
- `org.graalvm.python:python`
131+
- `org.graalvm.python:python-embedding`
132+
133+
The plugin can be configured in the `graalPy` block:
134+
135+
- The **packages** element declares a list of third-party Python packages to be downloaded and installed by the plugin.
136+
The Python packages and their versions are specified as if used with `pip`.
125137
```
126138
graalPy {
127139
packages = ["termcolor==2.2"]
128140
...
129141
}
130142
```
131143
- The **pythonHome** subsection declares what parts of the standard library should be deployed.
132-
133144
Each element in the `includes` and `excludes` list is interpreted as a Java-like regular expression specifying which file paths should be included or excluded.
134145
```
135146
graalPy {
@@ -141,15 +152,23 @@ The **packages** element declares a list of third-party Python packages to be do
141152
}
142153
```
143154
- If the **pythonResourcesDirectory** element is specified, then the given directory is used as an [external directory](#external-directory) and no Java resources are embedded.
144-
Remember to use the appropriate `GraalPyResources` API to create the Context.
155+
Remember to use the appropriate `GraalPyResources` API to create the Context.
145156
```
146157
graalPy {
147158
pythonResourcesDirectory = file("$rootDir/python-resources")
148159
...
149160
}
150161
```
162+
- Boolean flag **community** switches the automatically injected
163+
dependency `org.graalvm.python:python` to the community build: `org.graalvm.python:python-community`.
164+
```
165+
graalPy {
166+
community = true
167+
...
168+
}
169+
```
151170

152171
### Related Documentation
153172

154173
* [Embedding Graal languages in Java](https://www.graalvm.org/reference-manual/embed-languages/)
155-
* [Permissions for Python Embeddings](Embedding-Permissions.md)
174+
* [Permissions for Python Embeddings](Embedding-Permissions.md)

graalpython/com.oracle.graal.python.test/src/tests/standalone/gradle/build/build.gradle

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -12,10 +12,6 @@ application {
1212
mainClass = "org.example.GraalPy"
1313
}
1414

15-
dependencies {
16-
implementation("org.graalvm.python:python-community:24.2.0")
17-
}
18-
1915
run {
2016
enableAssertions = true
2117
}

graalpython/com.oracle.graal.python.test/src/tests/standalone/gradle/build/build.gradle.kts

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,3 @@ val r = tasks.run.get()
1717
r.enableAssertions = true
1818
r.outputs.upToDateWhen {false}
1919

20-
dependencies {
21-
implementation("org.graalvm.python:python-community:24.2.0")
22-
}

graalpython/com.oracle.graal.python.test/src/tests/standalone/test_gradle_plugin.py

Lines changed: 176 additions & 144 deletions
Large diffs are not rendered by default.

graalpython/com.oracle.graal.python.test/src/tests/standalone/test_maven_plugin.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,7 @@
4646
from tests.standalone import util
4747

4848

49-
class MavenPluginTest(util.PolyglotAppTestBase):
49+
class MavenPluginTest(util.BuildToolTestBase):
5050

5151
def generate_app(self, tmpdir, target_dir, target_name, pom_template=None):
5252
cmd = util.GLOBAL_MVN_CMD + [

graalpython/com.oracle.graal.python.test/src/tests/standalone/util.py

Lines changed: 78 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,9 @@
4343
import sys
4444
import unittest
4545
import urllib.parse
46+
import tempfile
47+
from abc import ABC, abstractmethod
48+
from typing import Optional
4649

4750
MAVEN_VERSION = "3.9.8"
4851
GLOBAL_MVN_CMD = [shutil.which('mvn'), "--batch-mode"]
@@ -54,7 +57,60 @@
5457
is_maven_plugin_test_enabled = 'ENABLE_MAVEN_PLUGIN_UNITTESTS' in os.environ and os.environ['ENABLE_MAVEN_PLUGIN_UNITTESTS'] == "true"
5558
is_gradle_plugin_test_enabled = 'ENABLE_GRADLE_PLUGIN_UNITTESTS' in os.environ and os.environ['ENABLE_GRADLE_PLUGIN_UNITTESTS'] == "true"
5659

57-
class PolyglotAppTestBase(unittest.TestCase):
60+
61+
class TemporaryTestDirectory():
62+
def __init__(self):
63+
if 'GRAALPY_UNITTESTS_TMPDIR_NO_CLEAN' in os.environ:
64+
self.ctx = None
65+
self.name = tempfile.mkdtemp()
66+
print(f"Running test in {self.name}")
67+
else:
68+
self.ctx = tempfile.TemporaryDirectory()
69+
self.name = self.ctx.name
70+
71+
def __enter__(self):
72+
return self.name
73+
74+
def __exit__(self, exc_type, exc_val, exc_tb):
75+
if self.ctx:
76+
self.ctx.__exit__(exc_type, exc_val, exc_tb)
77+
78+
class LoggerBase(ABC):
79+
def log_block(self, name, text):
80+
self.log("=" * 80)
81+
self.log(f"==> {name}:")
82+
self.log(text)
83+
self.log("=" * 80)
84+
85+
@abstractmethod
86+
def log(self, msg, newline=True):
87+
pass
88+
89+
class Logger(LoggerBase):
90+
def __init__(self):
91+
self.data = ''
92+
93+
def log(self, msg, newline=True):
94+
self.data += msg + ('\n' if newline else '')
95+
96+
def __str__(self):
97+
two_lines = ("=" * 80 + "\n") * 2
98+
return two_lines + "Test execution log:\n" + self.data + "\n" + two_lines
99+
100+
class NullLogger(LoggerBase):
101+
def log(self, msg, newline=True):
102+
pass
103+
104+
class StdOutLogger(LoggerBase):
105+
def __init__(self, delegate:LoggerBase):
106+
self.delegate = delegate
107+
108+
def log(self, msg, newline=True):
109+
print(msg)
110+
self.delegate.log(msg, newline=newline)
111+
112+
113+
class BuildToolTestBase(unittest.TestCase):
58114
@classmethod
59115
def setUpClass(cls):
60116
if not is_maven_plugin_test_enabled and not is_gradle_plugin_test_enabled:
@@ -72,14 +128,14 @@ def setUpClass(cls):
72128
url = urllib.parse.urlparse(custom_repo)
73129
if url.scheme == "file":
74130
jar = os.path.join(
75-
url.path,
131+
urllib.parse.unquote(url.path),
76132
cls.archetypeGroupId.replace(".", os.path.sep),
77133
cls.archetypeArtifactId,
78134
cls.graalvmVersion,
79135
f"{cls.archetypeArtifactId}-{cls.graalvmVersion}.jar",
80136
)
81137
pom = os.path.join(
82-
url.path,
138+
urllib.parse.unquote(url.path),
83139
cls.archetypeGroupId.replace(".", os.path.sep),
84140
cls.archetypeArtifactId,
85141
cls.graalvmVersion,
@@ -96,18 +152,18 @@ def setUpClass(cls):
96152
"-DcreateChecksum=true",
97153
]
98154
out, return_code = run_cmd(cmd, cls.env)
99-
assert return_code == 0
155+
assert return_code == 0, out
100156

101157
jar = os.path.join(
102-
url.path,
158+
urllib.parse.unquote(url.path),
103159
cls.archetypeGroupId.replace(".", os.path.sep),
104160
cls.pluginArtifactId,
105161
cls.graalvmVersion,
106162
f"{cls.pluginArtifactId}-{cls.graalvmVersion}.jar",
107163
)
108164

109165
pom = os.path.join(
110-
url.path,
166+
urllib.parse.unquote(url.path),
111167
cls.archetypeGroupId.replace(".", os.path.sep),
112168
cls.pluginArtifactId,
113169
cls.graalvmVersion,
@@ -125,10 +181,12 @@ def setUpClass(cls):
125181
"-DcreateChecksum=true",
126182
]
127183
out, return_code = run_cmd(cmd, cls.env)
128-
assert return_code == 0
184+
assert return_code == 0, out
129185
break
130186

131-
def run_cmd(cmd, env, cwd=None, print_out=False, gradle=False):
187+
def run_cmd(cmd, env, cwd=None, print_out=False, gradle=False, logger:LoggerBase=NullLogger()):
188+
if print_out:
189+
logger = StdOutLogger(logger)
132190
out = []
133191
out.append(f"Executing:\n {cmd=}\n")
134192
prev_java_home = None
@@ -139,27 +197,27 @@ def run_cmd(cmd, env, cwd=None, print_out=False, gradle=False):
139197
env["JAVA_HOME"] = env["GRADLE_JAVA_HOME"]
140198

141199
try:
200+
logger.log(f"Executing command: {' '.join(cmd)}")
142201
process = subprocess.Popen(cmd, env=env, cwd=cwd, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, universal_newlines=True, text=True, errors='backslashreplace')
143-
if print_out:
144-
print("============== output =============")
145202
for line in iter(process.stdout.readline, ""):
146203
out.append(line)
147-
if print_out:
148-
print(line, end="")
149-
if print_out:
150-
print("\n========== end of output ==========")
151-
return "".join(out), process.wait()
204+
out_str = "".join(out)
205+
logger.log_block("output", out_str)
206+
return out_str, process.wait()
152207
finally:
153208
if prev_java_home:
154209
env["JAVA_HOME"] = prev_java_home
155210

156-
def check_ouput(txt, out, contains=True):
211+
def check_ouput(txt, out, contains=True, logger: Optional[LoggerBase] =None):
212+
# if logger is passed, we assume that it already contains the output
157213
if contains and txt not in out:
158-
print_output(out, f"expected '{txt}' in output")
159-
assert False
214+
if not logger:
215+
print_output(out, f"expected '{txt}' in output")
216+
assert False, f"expected '{txt}' in output. \n{logger}"
160217
elif not contains and txt in out:
161-
print_output(out, f"did not expect '{txt}' in output")
162-
assert False
218+
if not logger:
219+
print_output(out, f"did not expect '{txt}' in output")
220+
assert False, f"did not expect '{txt}' in output. {logger}"
163221

164222
def print_output(out, err_msg=None):
165223
print("============== output =============")

graalpython/org.graalvm.python.embedding.tools/src/org/graalvm/python/embedding/tools/vfs/VFSUtils.java

Lines changed: 33 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -40,9 +40,6 @@
4040
*/
4141
package org.graalvm.python.embedding.tools.vfs;
4242

43-
import org.graalvm.python.embedding.tools.exec.GraalPyRunner;
44-
import org.graalvm.python.embedding.tools.exec.SubprocessLog;
45-
4643
import java.io.File;
4744
import java.io.FileWriter;
4845
import java.io.IOException;
@@ -55,18 +52,22 @@
5552
import java.nio.file.attribute.BasicFileAttributes;
5653
import java.nio.file.attribute.PosixFilePermission;
5754
import java.util.ArrayList;
58-
import java.util.Arrays;
5955
import java.util.Collection;
6056
import java.util.Comparator;
6157
import java.util.HashSet;
6258
import java.util.Iterator;
6359
import java.util.List;
6460
import java.util.Set;
61+
import java.util.TreeSet;
62+
import java.util.function.Consumer;
6563
import java.util.function.Predicate;
6664
import java.util.regex.Matcher;
6765
import java.util.regex.Pattern;
6866
import java.util.stream.Collectors;
6967

68+
import org.graalvm.python.embedding.tools.exec.GraalPyRunner;
69+
import org.graalvm.python.embedding.tools.exec.SubprocessLog;
70+
7071
public final class VFSUtils {
7172

7273
public static final String VFS_ROOT = "org.graalvm.python.vfs";
@@ -121,34 +122,46 @@ private static void createParentDirectories(Path path) throws IOException {
121122
}
122123

123124
public static void generateVFSFilesList(Path vfs) throws IOException {
125+
TreeSet<String> entriesSorted = new TreeSet<>();
126+
generateVFSFilesList(vfs, entriesSorted, null);
124127
Path filesList = vfs.resolve(VFS_FILESLIST);
128+
Files.write(filesList, entriesSorted);
129+
}
130+
131+
// Note: forward slash is not valid file/dir name character on Windows,
132+
// but backslash is valid file/dir name character on UNIX
133+
private static final boolean REPLACE_BACKSLASHES = File.separatorChar == '\\';
134+
135+
private static String normalizeResourcePath(String path) {
136+
return REPLACE_BACKSLASHES ? path.replace("\\", "/") : path;
137+
}
138+
139+
/**
140+
* Adds the VFS filelist entries to given set. Caller may provide a non-empty set.
141+
*/
142+
public static void generateVFSFilesList(Path vfs, Set<String> ret, Consumer<String> duplicateHandler) throws IOException {
125143
if (!Files.isDirectory(vfs)) {
126-
throw new IOException(String.format("'%s' has to exist and be a directory.\n", vfs.toString()));
144+
throw new IOException(String.format("'%s' has to exist and be a directory.\n", vfs));
127145
}
128-
var ret = new HashSet<String>();
129146
String rootPath = makeDirPath(vfs.toAbsolutePath());
130147
int rootEndIdx = rootPath.lastIndexOf(File.separator, rootPath.lastIndexOf(File.separator) - 1);
131-
ret.add(rootPath.substring(rootEndIdx));
148+
ret.add(normalizeResourcePath(rootPath.substring(rootEndIdx)));
132149
try (var s = Files.walk(vfs)) {
133150
s.forEach(p -> {
151+
String entry = null;
134152
if (Files.isDirectory(p)) {
135153
String dirPath = makeDirPath(p.toAbsolutePath());
136-
ret.add(dirPath.substring(rootEndIdx));
154+
entry = dirPath.substring(rootEndIdx);
137155
} else if (Files.isRegularFile(p)) {
138-
ret.add(p.toAbsolutePath().toString().substring(rootEndIdx));
156+
entry = p.toAbsolutePath().toString().substring(rootEndIdx);
139157
}
140-
});
141-
}
142-
String[] a = ret.toArray(new String[ret.size()]);
143-
Arrays.sort(a);
144-
try (var wr = new FileWriter(filesList.toFile())) {
145-
for (String f : a) {
146-
if (f.charAt(0) == '\\') {
147-
f = f.replace("\\", "/");
158+
if (entry != null) {
159+
entry = normalizeResourcePath(entry);
160+
if (!ret.add(entry) && duplicateHandler != null) {
161+
duplicateHandler.accept(entry);
162+
}
148163
}
149-
wr.write(f);
150-
wr.write("\n");
151-
}
164+
});
152165
}
153166
}
154167

0 commit comments

Comments
 (0)