Skip to content

Commit c2d0c87

Browse files
authored
Merge pull request #30 from ih0r-d/feat/requirement-file-support
feat: requirements.txt support for maven plugin
2 parents b7d9f78 + 9b7b675 commit c2d0c87

File tree

10 files changed

+371
-100
lines changed

10 files changed

+371
-100
lines changed

graalpy-maven-plugin/src/main/java/org/graalvm/python/maven/plugin/AbstractGraalPyMojo.java

Lines changed: 36 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@
4040
*/
4141
package org.graalvm.python.maven.plugin;
4242

43+
import java.util.ArrayList;
4344
import org.apache.maven.artifact.Artifact;
4445
import org.apache.maven.artifact.DefaultArtifact;
4546
import org.apache.maven.artifact.handler.DefaultArtifactHandler;
@@ -110,6 +111,9 @@ public AbstractGraalPyMojo(ProjectBuilder projectBuilder) {
110111
@Parameter
111112
List<String> packages;
112113

114+
@Parameter(property = "requirementsFile")
115+
String requirementsFile;
116+
113117
@SuppressFBWarnings("UUF_UNUSED_FIELD")
114118
public static class PythonHome {
115119
@SuppressWarnings("unused")
@@ -124,7 +128,7 @@ public static class PythonHome {
124128
@Parameter(defaultValue = "${session}", readonly = true, required = true)
125129
private MavenSession session;
126130

127-
private ProjectBuilder projectBuilder;
131+
private final ProjectBuilder projectBuilder;
128132

129133
private Set<String> launcherClassPath;
130134

@@ -153,9 +157,17 @@ protected void preExec(boolean enableWarnings) throws MojoExecutionException {
153157
externalDirectory = normalizeEmpty(externalDirectory);
154158
resourceDirectory = normalizeEmpty(resourceDirectory);
155159
graalPyLockFile = normalizeEmpty(graalPyLockFile);
156-
packages = packages != null
157-
? packages.stream().filter(p -> p != null && !p.trim().isEmpty()).toList()
158-
: List.of();
160+
Path reqFilePath = resolveReqFile();
161+
if (reqFilePath != null) {
162+
getLog().info("GraalPy requirements file: " + reqFilePath);
163+
if (packages != null) {
164+
throw new MojoExecutionException("Cannot use <packages> and <requirementsFile> at the same time. "
165+
+ "New option <requirementsFile> is a replacement for using <packages> with list of inline <package>.");
166+
}
167+
packages = new ArrayList<>();
168+
} else if (packages != null) {
169+
packages = packages.stream().filter(p -> p != null && !p.trim().isEmpty()).toList();
170+
}
159171

160172
if (pythonResourcesDirectory != null) {
161173
if (externalDirectory != null) {
@@ -201,6 +213,22 @@ protected void preExec(boolean enableWarnings) throws MojoExecutionException {
201213
}
202214
}
203215

216+
protected Path resolveReqFile() throws MojoExecutionException {
217+
if (requirementsFile == null || requirementsFile.isBlank()) {
218+
return null;
219+
}
220+
221+
Path path = Path.of(requirementsFile);
222+
Path finalPath = path.isAbsolute() ? path : project.getBasedir().toPath().resolve(path).normalize();
223+
224+
if (!Files.exists(finalPath)) {
225+
throw new MojoExecutionException("The configured requirementsFile does not exist: " + finalPath
226+
+ "\nPlease provide a valid path to a pip-compatible requirements file.");
227+
}
228+
229+
return finalPath;
230+
}
231+
204232
protected void postExec() throws MojoExecutionException {
205233
for (Resource r : project.getBuild().getResources()) {
206234
if (Files.exists(Path.of(r.getDirectory(), resourceDirectory, "proj"))) {
@@ -249,12 +277,11 @@ private static String normalizeEmpty(String s) {
249277
}
250278

251279
protected Launcher createLauncher() {
252-
Launcher launcherArg = new Launcher(getLauncherPath()) {
280+
return new Launcher(getLauncherPath()) {
253281
public Set<String> computeClassPath() throws IOException {
254282
return calculateLauncherClasspath(project);
255283
}
256284
};
257-
return launcherArg;
258285
}
259286

260287
protected Path getLockFile() {
@@ -293,7 +320,8 @@ protected static String getGraalPyVersion(MavenProject project) throws IOExcepti
293320

294321
private static Artifact getGraalPyArtifact(MavenProject project) throws IOException {
295322
var projectArtifacts = resolveProjectDependencies(project);
296-
Artifact graalPyArtifact = projectArtifacts.stream().filter(a -> isPythonArtifact(a)).findFirst().orElse(null);
323+
Artifact graalPyArtifact = projectArtifacts.stream().filter(AbstractGraalPyMojo::isPythonArtifact).findFirst()
324+
.orElse(null);
297325
return Optional.ofNullable(graalPyArtifact).orElseThrow(() -> new IOException(
298326
"Missing GraalPy dependency. Please add to your pom either %s:%s or %s:%s".formatted(POLYGLOT_GROUP_ID,
299327
PYTHON_COMMUNITY_ARTIFACT_ID, POLYGLOT_GROUP_ID, PYTHON_ARTIFACT_ID)));
@@ -326,6 +354,7 @@ private Set<String> calculateLauncherClasspath(MavenProject project) throws IOEx
326354
&& PYTHON_LAUNCHER_ARTIFACT_ID.equals(a.getArtifactId()))
327355
.findFirst().orElse(null);
328356
// python-launcher artifact
357+
assert graalPyLauncherArtifact != null;
329358
launcherClassPath.add(graalPyLauncherArtifact.getFile().getAbsolutePath());
330359
// and transitively all its dependencies
331360
launcherClassPath.addAll(resolveDependencies(graalPyLauncherArtifact));

graalpy-maven-plugin/src/main/java/org/graalvm/python/maven/plugin/InstallPackagesMojo.java

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -40,12 +40,14 @@
4040
*/
4141
package org.graalvm.python.maven.plugin;
4242

43+
import java.nio.file.Files;
4344
import org.apache.maven.plugin.MojoExecutionException;
4445
import org.apache.maven.plugins.annotations.LifecyclePhase;
4546
import org.apache.maven.plugins.annotations.Mojo;
4647
import org.apache.maven.plugins.annotations.ResolutionScope;
4748
import org.apache.maven.project.ProjectBuilder;
4849
import org.graalvm.python.embedding.tools.vfs.VFSUtils;
50+
import org.graalvm.python.embedding.tools.vfs.VFSUtils.Launcher;
4951
import org.graalvm.python.embedding.tools.vfs.VFSUtils.PackagesChangedException;
5052

5153
import javax.inject.Inject;
@@ -98,9 +100,22 @@ private void manageVenv() throws MojoExecutionException {
98100
Path venvDirectory = getVenvDirectory();
99101
MavenDelegateLog log = new MavenDelegateLog(getLog());
100102
Path lockFile = getLockFile();
103+
Path reqFile = resolveReqFile();
104+
105+
boolean emptyPackages = packages == null || packages.isEmpty();
106+
boolean hasReqFile = reqFile != null && Files.exists(reqFile);
107+
boolean hasLockFile = lockFile != null && Files.exists(lockFile);
108+
109+
if (emptyPackages && !hasReqFile) {
110+
if (hasLockFile) {
111+
throw new MojoExecutionException(
112+
"Lock file is present, but no Python packages or requirements.txt are configured.");
113+
}
114+
return;
115+
}
101116
try {
102117
VFSUtils.createVenv(venvDirectory, packages, lockFile, MISSING_LOCK_FILE_WARNING, createLauncher(),
103-
getGraalPyVersion(project), log);
118+
getGraalPyVersion(project), log, reqFile);
104119
} catch (PackagesChangedException pce) {
105120
String pluginPkgsString = pce.getPluginPackages().isEmpty()
106121
? "None"

graalpy-maven-plugin/src/main/java/org/graalvm/python/maven/plugin/LockPackagesMojo.java

Lines changed: 49 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,47 @@ public class LockPackagesMojo extends AbstractGraalPyMojo {
6464
This file contains a list of all required Python packages with their specific versions,
6565
based on the packages defined in the plugin configuration and their dependencies.
6666
""";
67+
public static final String MISSING_DEPENDENCY_CONFIGURATION_ERROR = """
68+
In order to run the lock-packages goal there have to be python packages declared in the graalpy-maven-plugin configuration.
69+
70+
You must configure Python dependencies in one of the following ways:
71+
72+
Option 1: Use <packages> with inline versioned package entries:
73+
74+
<plugin>
75+
<groupId>org.graalvm.python</groupId>
76+
<artifactId>graalpy-maven-plugin</artifactId>
77+
<configuration>
78+
<packages>
79+
<package>{package_name}=={package_version}</package>
80+
</packages>
81+
...
82+
</configuration>
83+
</plugin>
84+
85+
Option 2: Use a pip-compatible requirements.txt file:
86+
87+
<plugin>
88+
<groupId>org.graalvm.python</groupId>
89+
<artifactId>graalpy-maven-plugin</artifactId>
90+
<configuration>
91+
<requirementsFile>requirements.txt</requirementsFile>
92+
...
93+
</configuration>
94+
</plugin>
95+
96+
IMPORTANT:
97+
• The requirementsFile workflow follows pip's native behavior.
98+
• GraalPy lock files are NOT used or generated when requirementsFile is specified.
99+
• The 'lock-packages' goal is NOT supported with <requirementsFile>.
100+
• Users are expected to manage locking / freezing themselves using pip conventions (e.g., pip freeze).
101+
• Do not define both <packages> and <requirementsFile> at the same time.
102+
• The <configuration> section must be declared on the graalpy-maven-plugin itself,
103+
not inside a specific execution.
104+
105+
For more details, see:
106+
https://github.com/oracle/graalpython/blob/master/docs/user/Embedding-Build-Tools.md
107+
""";
67108

68109
@Inject
69110
public LockPackagesMojo(ProjectBuilder projectBuilder) {
@@ -94,32 +135,16 @@ protected void manageVenv() throws MojoExecutionException {
94135
}
95136

96137
private void checkEmptyPackages() throws MojoExecutionException {
97-
if ((packages == null || packages.isEmpty())) {
98-
getLog().error("");
99-
getLog().error(
100-
"In order to run the lock-packages goal there have to be python packages declared in the graalpy-maven-plugin configuration.");
101-
getLog().error("");
102-
getLog().error(
103-
"NOTE that the <configuration> section has to be declared for the whole graalpy-maven-plugin");
104-
getLog().error("and not specifically for the process-graalpy-resources execution goal.");
138+
Path reqFilePath = resolveReqFile();
139+
boolean emptyPackages = packages == null || packages.isEmpty();
140+
boolean requirementsExists = reqFilePath != null;
141+
// Disallow lock-packages when no packages OR when requirementsFile is used
142+
if (emptyPackages || requirementsExists) {
105143
getLog().error("");
106-
getLog().error("Please add the <packages> section to your configuration as follows:");
107-
getLog().error("<plugin>");
108-
getLog().error(" <groupId>org.graalvm.python</groupId>");
109-
getLog().error(" <artifactId>graalpy-maven-plugin</artifactId>");
110-
getLog().error(" <configuration>");
111-
getLog().error(" <packages>");
112-
getLog().error(" <package>{package_name}=={package_version}</package>");
113-
getLog().error(" </packages>");
114-
getLog().error(" ...");
115-
getLog().error(" </configuration>");
144+
getLog().error(MISSING_DEPENDENCY_CONFIGURATION_ERROR);
116145
getLog().error("");
117-
118-
getLog().error(
119-
"For more information, please refer to https://github.com/oracle/graalpython/blob/master/docs/user/Embedding-Build-Tools.md");
120-
getLog().error("");
121-
122-
throw new MojoExecutionException("missing python packages in plugin configuration");
146+
throw new MojoExecutionException("In order to run the lock-packages goal there have to be python packages "
147+
+ "declared in the graalpy-maven-plugin configuration");
123148
}
124149
}
125150
}
Lines changed: 99 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,99 @@
1+
<?xml version="1.0" encoding="UTF-8"?>
2+
<!--
3+
Copyright (c) 2025, Oracle and/or its affiliates. All rights reserved.
4+
DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
5+
6+
The Universal Permissive License (UPL), Version 1.0
7+
8+
Subject to the condition set forth below, permission is hereby granted to any
9+
person obtaining a copy of this software, associated documentation and/or
10+
data (collectively the "Software"), free of charge and under any and all
11+
copyright rights in the Software, and any and all patent rights owned or
12+
freely licensable by each licensor hereunder covering either (i) the
13+
unmodified Software as contributed to or provided by such licensor, or (ii)
14+
the Larger Works (as defined below), to deal in both
15+
16+
(a) the Software, and
17+
18+
(b) any piece of software and/or hardware listed in the lrgrwrks.txt file if
19+
one is included with the Software each a "Larger Work" to which the Software
20+
is contributed by such licensors),
21+
22+
without restriction, including without limitation the rights to copy, create
23+
derivative works of, display, perform, and distribute the Software and make,
24+
use, sell, offer for sale, import, export, have made, and have sold the
25+
Software and the Larger Work(s), and to sublicense the foregoing rights on
26+
either these or other terms.
27+
28+
This license is subject to the following condition:
29+
30+
The above copyright notice and either this complete permission notice or at a
31+
minimum a reference to the UPL must be included in all copies or substantial
32+
portions of the Software.
33+
34+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
35+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
36+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
37+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
38+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
39+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
40+
SOFTWARE.
41+
-->
42+
<project xmlns="http://maven.apache.org/POM/4.0.0"
43+
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
44+
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
45+
<modelVersion>4.0.0</modelVersion>
46+
47+
<groupId>archetype.it</groupId>
48+
<artifactId>requirements_packages</artifactId>
49+
<version>1.0-SNAPSHOT</version>
50+
<packaging>jar</packaging>
51+
52+
<dependencies>
53+
<dependency>
54+
<groupId>junit</groupId>
55+
<artifactId>junit</artifactId>
56+
<version>3.8.1</version>
57+
<scope>test</scope>
58+
</dependency>
59+
<dependency>
60+
<groupId>org.graalvm.polyglot</groupId>
61+
<artifactId>python</artifactId>
62+
<version>${env.GRAALPY_VERSION}</version>
63+
<type>pom</type>
64+
</dependency>
65+
<dependency>
66+
<groupId>org.graalvm.python</groupId>
67+
<artifactId>python-launcher</artifactId>
68+
<version>${env.GRAALPY_VERSION}</version>
69+
</dependency>
70+
<dependency>
71+
<groupId>org.graalvm.python</groupId>
72+
<artifactId>python-embedding</artifactId>
73+
<version>${env.GRAALPY_VERSION}</version>
74+
</dependency>
75+
</dependencies>
76+
77+
<build>
78+
<plugins>
79+
<plugin>
80+
<groupId>org.graalvm.python</groupId>
81+
<artifactId>graalpy-maven-plugin</artifactId>
82+
<version>${env.GRAALPY_VERSION}</version>
83+
<executions>
84+
<execution>
85+
<goals>
86+
<goal>process-graalpy-resources</goal>
87+
</goals>
88+
</execution>
89+
</executions>
90+
<configuration>
91+
<resourceDirectory>
92+
GRAALPY-VFS/${project.groupId}/${project.artifactId}
93+
</resourceDirectory>
94+
<requirementsFile>requirements.txt</requirementsFile>
95+
</configuration>
96+
</plugin>
97+
</plugins>
98+
</build>
99+
</project>

integration-tests/test_maven_plugin.py

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -835,6 +835,42 @@ def test_multiple_namespaced_vfs(self):
835835
assert return_code == 0, log
836836

837837

838+
def test_requirements_txt_packages(self):
839+
with util.TemporaryTestDirectory() as dir:
840+
target_name = "requirements_packages"
841+
target_dir = os.path.join(str(dir), target_name)
842+
pom_template = os.path.join(
843+
os.path.dirname(__file__),
844+
"prepare_venv_requirements_pom.xml",
845+
)
846+
self.generate_app(dir, target_dir, target_name, pom_template)
847+
848+
requirements_txt = os.path.join(target_dir, "requirements.txt")
849+
if not os.path.exists(requirements_txt):
850+
with open(requirements_txt, "w", encoding="utf-8") as f:
851+
f.write("termcolor==2.4.0\n")
852+
853+
mvnw_cmd = util.get_mvn_wrapper(target_dir, self.env)
854+
855+
cmd = mvnw_cmd + ["process-resources"]
856+
out, return_code = util.run_cmd(cmd, self.env, cwd=target_dir)
857+
util.check_ouput("BUILD SUCCESS", out)
858+
assert return_code == 0
859+
860+
lock_file = os.path.join(target_dir, "graalpy.lock")
861+
assert not os.path.exists(lock_file), "lock-file must NOT exist for requirements.txt mode"
862+
cmd = mvnw_cmd + ["package", "-DmainClass=it.pkg.GraalPy"]
863+
out, return_code = util.run_cmd(cmd, self.env, cwd=target_dir)
864+
util.check_ouput("BUILD SUCCESS", out)
865+
assert return_code == 0
866+
867+
cmd = mvnw_cmd + ["exec:java", "-Dexec.mainClass=it.pkg.GraalPy"]
868+
out, return_code = util.run_cmd(cmd, self.env, cwd=target_dir)
869+
util.check_ouput("hello java", out)
870+
util.check_ouput("BUILD SUCCESS", out)
871+
assert return_code == 0
872+
873+
838874
if __name__ == "__main__":
839875
run_path = os.path.join(os.path.abspath(__file__), 'run.py')
840876
print(f"Run this file using the run.py driver ({run_path})")

integration-tests/util.py

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -60,10 +60,17 @@
6060
native_image_mode = "all"
6161
extra_maven_repos = []
6262

63+
def _native_image_allowed_on_platform():
64+
return sys.platform != "darwin"
65+
6366
def native_image_all():
67+
if not _native_image_allowed_on_platform():
68+
return False
6469
return native_image_mode == "all"
6570

6671
def native_image_smoke():
72+
if not _native_image_allowed_on_platform():
73+
return False
6774
return native_image_mode in ("all", "smoke")
6875

6976
gradle_java_home = os.environ['JAVA_HOME']

0 commit comments

Comments
 (0)