Skip to content

Commit 32f68ad

Browse files
melixsteve-s
authored andcommitted
Polish GraalPy Gradle plugin
Fixes the GraalPy Gradle plugin to make it more robust: - it fixes up-to-date checking by making sure that all tasks declare their inputs and outputs - it fixes overlapping outputs which would cause tasks to be always out of date (each task should use a distinct output directory) - it fixes eager configuration of the project caused by dependency resolution - it fixes the scope of some dependencies which should be runtime only - it fixes how tasks are wired in resource processing, by using a source set dependency instead of adding explicit task dependencies - filelist is generated only when external resources directory is not specified - checkstyle and eclipseformat on the sources (not integrated in mx yet) New features: - introduce a `community` boolean property (default: false) to configure which GraalPy Maven artifact to inject - the version of the injected GraalPy Maven artifacts is always the same as the version of the GraalPy Gradle plugin Implementation: - introduces new MX project type: GradlePluginProject, which uses Gradle to build the plugin jars and also invokes validatePlugins as part of mx build. The Gradle build script as well as the plugin properties file are generated by GradlePluginProject during the build. - GradlePluginProject downloads Gradle if not available
1 parent c4ba335 commit 32f68ad

File tree

17 files changed

+1004
-465
lines changed

17 files changed

+1004
-465
lines changed

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)