diff --git a/pom.xml b/pom.xml
index 85f0693..9bc2d59 100644
--- a/pom.xml
+++ b/pom.xml
@@ -41,12 +41,25 @@
2.0.17
3.0.0
1.22.0
- 6.0.0
+ 6.0.3
+ 2.3.0
5.20.0
3.27.6
3.5.0
+
+
+
+ org.junit
+ junit-bom
+ ${version.junit}
+ pom
+ import
+
+
+
+
eu.maveniverse.maven.mima
@@ -98,7 +111,17 @@
org.junit.jupiter
junit-jupiter
- ${version.junit}
+ test
+
+
+ org.junit.platform
+ junit-platform-launcher
+ test
+
+
+ org.junit-pioneer
+ junit-pioneer
+ ${version.junit-pioneer}
test
@@ -279,6 +302,9 @@
org.apache.maven.plugins
maven-surefire-plugin
+
+ --add-opens java.base/java.util=ALL-UNNAMED --add-opens java.base/java.lang=ALL-UNNAMED
+
org.apache.maven.plugins
diff --git a/src/main/java/org/codejive/jpm/Main.java b/src/main/java/org/codejive/jpm/Main.java
index bab71fd..efe7c5b 100644
--- a/src/main/java/org/codejive/jpm/Main.java
+++ b/src/main/java/org/codejive/jpm/Main.java
@@ -15,6 +15,7 @@
import java.io.*;
import java.net.MalformedURLException;
import java.net.URL;
+import java.nio.file.InvalidPathException;
import java.nio.file.Path;
import java.util.*;
import java.util.concurrent.Callable;
@@ -90,7 +91,7 @@ public Integer call() throws Exception {
Jpm.builder()
.directory(artifactsMixin.directory)
.noLinks(artifactsMixin.noLinks)
- .cacheDir(artifactsMixin.cacheDir)
+ .cacheDir(artifactsMixin.getCacheDir())
.build()
.copy(
artifactsMixin.artifactNames,
@@ -163,7 +164,7 @@ public Integer call() throws Exception {
Jpm.builder()
.directory(depsMixin.directory)
.noLinks(depsMixin.noLinks)
- .cacheDir(depsMixin.cacheDir)
+ .cacheDir(depsMixin.getCacheDir())
.appFile(appInfoFileMixin.appInfoFile)
.build()
.install(
@@ -177,7 +178,7 @@ public Integer call() throws Exception {
Jpm.builder()
.directory(depsMixin.directory)
.noLinks(depsMixin.noLinks)
- .cacheDir(depsMixin.cacheDir)
+ .cacheDir(depsMixin.getCacheDir())
.appFile(appInfoFileMixin.appInfoFile)
.build()
.copy(
@@ -219,7 +220,7 @@ String[] search(String artifactPattern) {
return Jpm.builder()
.directory(depsMixin.directory)
.noLinks(depsMixin.noLinks)
- .cacheDir(depsMixin.cacheDir)
+ .cacheDir(depsMixin.getCacheDir())
.appFile(appInfoFileMixin.appInfoFile)
.build()
.search(artifactPattern, Math.min(max, 200), backend);
@@ -320,7 +321,7 @@ public Integer call() throws Exception {
Jpm.builder()
.directory(optionalArtifactsMixin.directory)
.noLinks(optionalArtifactsMixin.noLinks)
- .cacheDir(optionalArtifactsMixin.cacheDir)
+ .cacheDir(optionalArtifactsMixin.getCacheDir())
.appFile(appInfoFileMixin.appInfoFile)
.build()
.install(
@@ -351,7 +352,7 @@ public Integer call() throws Exception {
Jpm.builder()
.directory(optionalArtifactsMixin.directory)
.noLinks(optionalArtifactsMixin.noLinks)
- .cacheDir(optionalArtifactsMixin.cacheDir)
+ .cacheDir(optionalArtifactsMixin.getCacheDir())
.appFile(appInfoFileMixin.appInfoFile)
.build()
.path(
@@ -409,7 +410,7 @@ public Integer call() throws Exception {
return Jpm.builder()
.directory(depsMixin.directory)
.noLinks(depsMixin.noLinks)
- .cacheDir(depsMixin.cacheDir)
+ .cacheDir(depsMixin.getCacheDir())
.appFile(appInfoFileMixin.appInfoFile)
.verbose(!quietMixin.quiet)
.build()
@@ -468,7 +469,7 @@ public Integer call() throws Exception {
Jpm.builder()
.directory(depsMixin.directory)
.noLinks(depsMixin.noLinks)
- .cacheDir(depsMixin.cacheDir)
+ .cacheDir(depsMixin.getCacheDir())
.appFile(appInfoFileMixin.appInfoFile)
.build()
.listActions();
@@ -514,7 +515,7 @@ public Integer call() throws Exception {
Jpm.builder()
.directory(depsMixin.directory)
.noLinks(depsMixin.noLinks)
- .cacheDir(depsMixin.cacheDir)
+ .cacheDir(depsMixin.getCacheDir())
.appFile(appInfoFileMixin.appInfoFile)
.verbose(!quietMixin.quiet)
.build()
@@ -548,7 +549,7 @@ public Integer call() throws Exception {
return Jpm.builder()
.directory(depsMixin.directory)
.noLinks(depsMixin.noLinks)
- .cacheDir(depsMixin.cacheDir)
+ .cacheDir(depsMixin.getCacheDir())
.appFile(appInfoFileMixin.appInfoFile)
.build()
.executeAction(actionName(), args, depsMixin.getRepositoryMap());
@@ -624,6 +625,23 @@ static class DepsMixin {
"Directory where downloaded artifacts will be cached (default: value of JPM_CACHE environment variable; whatever is set in Maven's settings.xml or $HOME/.m2/repository")
Path cacheDir;
+ Path getCacheDir() {
+ if (cacheDir != null) {
+ return cacheDir;
+ }
+ String envCache = System.getenv("JPM_CACHE");
+ if (envCache != null && !envCache.isEmpty()) {
+ try {
+ return Path.of(envCache);
+ } catch (InvalidPathException e) {
+ System.err.println(
+ "Warning: Invalid path in JPM_CACHE environment variable, ignoring: "
+ + envCache);
+ }
+ }
+ return null;
+ }
+
Map getRepositoryMap() {
Map repoMap = new HashMap<>();
for (String repo : repositories) {
diff --git a/src/test/java/org/codejive/jpm/MainCacheIntegrationTest.java b/src/test/java/org/codejive/jpm/MainCacheIntegrationTest.java
new file mode 100644
index 0000000..0adda31
--- /dev/null
+++ b/src/test/java/org/codejive/jpm/MainCacheIntegrationTest.java
@@ -0,0 +1,166 @@
+package org.codejive.jpm;
+
+import static org.assertj.core.api.Assertions.*;
+
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
+import java.io.PrintStream;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import org.junit.jupiter.api.AfterEach;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.io.TempDir;
+import org.junitpioneer.jupiter.ClearEnvironmentVariable;
+import org.junitpioneer.jupiter.SetEnvironmentVariable;
+import picocli.CommandLine;
+
+/** Integration tests for the --cache option and JPM_CACHE environment variable. */
+class MainCacheIntegrationTest {
+
+ @TempDir Path tempDir;
+ @TempDir Path cacheDir1;
+ @TempDir Path cacheDir2;
+
+ private String originalUserDir;
+ private PrintStream originalOut;
+ private PrintStream originalErr;
+ private ByteArrayOutputStream outContent;
+ private ByteArrayOutputStream errContent;
+
+ @BeforeEach
+ void setUp() {
+ originalUserDir = System.getProperty("user.dir");
+ System.setProperty("user.dir", tempDir.toString());
+ System.setProperty("picocli.ansi", "false");
+
+ // Capture stdout and stderr
+ originalOut = System.out;
+ originalErr = System.err;
+ outContent = new ByteArrayOutputStream();
+ errContent = new ByteArrayOutputStream();
+ System.setOut(new PrintStream(outContent));
+ System.setErr(new PrintStream(errContent));
+ }
+
+ @AfterEach
+ void tearDown() {
+ System.setProperty("user.dir", originalUserDir);
+ System.setOut(originalOut);
+ System.setErr(originalErr);
+ }
+
+ @Test
+ @ClearEnvironmentVariable(key = "JPM_CACHE")
+ void testPathCommandWithCacheOption() throws IOException {
+ // Create app.yml
+ createSimpleAppYml();
+
+ CommandLine cmd = Main.getCommandLine();
+ int exitCode = cmd.execute("path", "--cache", cacheDir1.toString());
+
+ // Exit code 0 or 1 is acceptable (1 means dependency not found, which is expected)
+ assertThat(exitCode).isIn(0, 1);
+ }
+
+ @Test
+ @SetEnvironmentVariable(key = "JPM_CACHE", value = "/tmp/env-cache")
+ void testPathCommandWithEnvironmentVariable() throws IOException {
+ // Create app.yml
+ createSimpleAppYml();
+
+ CommandLine cmd = Main.getCommandLine();
+ int exitCode = cmd.execute("path");
+
+ // The command should succeed with JPM_CACHE set
+ assertThat(exitCode).isIn(0, 1);
+ }
+
+ @Test
+ @SetEnvironmentVariable(key = "JPM_CACHE", value = "/tmp/env-cache")
+ void testCopyCommandCacheOptionOverridesEnvironmentVariable() throws IOException {
+ CommandLine cmd = Main.getCommandLine();
+ int exitCode =
+ cmd.execute(
+ "copy", "--cache", cacheDir1.toString(), "--quiet", "fake:artifact:1.0.0");
+
+ // The command should use cacheDir1 (from --cache) not /tmp/env-cache
+ // Even though it will fail to resolve, it should parse correctly
+ assertThat(exitCode).isIn(0, 1); // May fail to resolve, but shouldn't crash
+ }
+
+ @Test
+ @ClearEnvironmentVariable(key = "JPM_CACHE")
+ void testInstallCommandWithShortCacheOption() throws IOException {
+ createSimpleAppYml();
+
+ CommandLine cmd = Main.getCommandLine();
+ int exitCode =
+ cmd.execute(
+ "install", "-c", cacheDir1.toString(), "--quiet", "fake:artifact:1.0.0");
+
+ // The -c short form should work the same as --cache
+ assertThat(exitCode).isIn(0, 1); // May fail to resolve, but shouldn't crash
+ }
+
+ @Test
+ void testCacheOptionInHelp() {
+ CommandLine cmd = Main.getCommandLine();
+ int exitCode = cmd.execute("copy", "--help");
+
+ // PicoCLI may return 0 or 2 for help depending on configuration
+ // What matters is that the help text is displayed
+ String output = outContent.toString() + errContent.toString();
+ assertThat(output)
+ .contains("-c, --cache")
+ .contains("Directory where downloaded artifacts will be cached")
+ .contains("JPM_CACHE");
+ }
+
+ @Test
+ @SetEnvironmentVariable(key = "JPM_CACHE", value = " ")
+ void testGetCacheDirWithWhitespaceOnlyEnvironmentVariable() throws IOException {
+ // An environment variable with only whitespace should be treated as empty
+ createSimpleAppYml();
+
+ CommandLine cmd = Main.getCommandLine();
+ // This should not crash - whitespace-only JPM_CACHE should be ignored
+ int exitCode = cmd.execute("path");
+
+ assertThat(exitCode).isIn(0, 1);
+ }
+
+ @Test
+ @ClearEnvironmentVariable(key = "JPM_CACHE")
+ void testCacheOptionWithRelativePath() throws IOException {
+ createSimpleAppYml();
+
+ CommandLine cmd = Main.getCommandLine();
+ int exitCode = cmd.execute("path", "--cache", "./my-cache");
+
+ assertThat(exitCode).isIn(0, 1);
+ // Should accept relative paths
+ }
+
+ @Test
+ @ClearEnvironmentVariable(key = "JPM_CACHE")
+ void testCacheOptionWithAbsolutePath() throws IOException {
+ createSimpleAppYml();
+
+ CommandLine cmd = Main.getCommandLine();
+ int exitCode = cmd.execute("path", "--cache", cacheDir1.toAbsolutePath().toString());
+
+ assertThat(exitCode).isIn(0, 1);
+ // Should accept absolute paths
+ }
+
+ private void createSimpleAppYml() throws IOException {
+ String yamlContent =
+ "dependencies:\n"
+ + " fake:dummy: \"1.2.3\"\n"
+ + "\n"
+ + "actions:\n"
+ + " build: \"echo building\"\n";
+ Files.writeString(tempDir.resolve("app.yml"), yamlContent);
+ }
+}
diff --git a/src/test/java/org/codejive/jpm/MainCacheOptionsTest.java b/src/test/java/org/codejive/jpm/MainCacheOptionsTest.java
new file mode 100644
index 0000000..33722a5
--- /dev/null
+++ b/src/test/java/org/codejive/jpm/MainCacheOptionsTest.java
@@ -0,0 +1,114 @@
+package org.codejive.jpm;
+
+import static org.assertj.core.api.Assertions.*;
+
+import java.io.ByteArrayOutputStream;
+import java.io.PrintStream;
+import java.nio.file.Path;
+import org.junit.jupiter.api.AfterEach;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.io.TempDir;
+import org.junitpioneer.jupiter.ClearEnvironmentVariable;
+import org.junitpioneer.jupiter.SetEnvironmentVariable;
+
+/** Tests for Main class CLI cache option parsing. */
+class MainCacheOptionsTest {
+
+ @TempDir Path tempCacheDir1;
+ @TempDir Path tempCacheDir2;
+
+ private PrintStream originalErr;
+ private ByteArrayOutputStream errContent;
+
+ @BeforeEach
+ void setUp() {
+ // Capture stderr to verify warning messages
+ originalErr = System.err;
+ errContent = new ByteArrayOutputStream();
+ System.setErr(new PrintStream(errContent));
+ }
+
+ @AfterEach
+ void tearDown() {
+ System.setErr(originalErr);
+ }
+
+ @Test
+ @ClearEnvironmentVariable(key = "JPM_CACHE")
+ void testGetCacheDirWithCommandLineOption() {
+ Main.DepsMixin mixin = new Main.DepsMixin();
+ mixin.cacheDir = tempCacheDir1;
+
+ Path result = mixin.getCacheDir();
+
+ assertThat(result).isEqualTo(tempCacheDir1);
+ assertThat(errContent.toString()).isEmpty(); // No warnings
+ }
+
+ @Test
+ @SetEnvironmentVariable(key = "JPM_CACHE", value = "/tmp/jpm-cache-from-env")
+ void testGetCacheDirFromEnvironmentVariable() {
+ Main.DepsMixin mixin = new Main.DepsMixin();
+ // Don't set mixin.cacheDir - it should use the env var
+
+ Path result = mixin.getCacheDir();
+
+ assertThat(result).isNotNull();
+ String expectedPath = Path.of("/tmp/jpm-cache-from-env").toString();
+ assertThat(result.toString()).isEqualTo(expectedPath);
+ assertThat(errContent.toString()).isEmpty(); // No warnings
+ }
+
+ @Test
+ @SetEnvironmentVariable(key = "JPM_CACHE", value = "/tmp/jpm-cache-from-env")
+ void testCommandLineOptionTakesPrecedenceOverEnvironmentVariable() {
+ Main.DepsMixin mixin = new Main.DepsMixin();
+ mixin.cacheDir = tempCacheDir1; // Command line option is set
+
+ Path result = mixin.getCacheDir();
+
+ // Should return the command line option, not the environment variable
+ assertThat(result).isEqualTo(tempCacheDir1);
+ assertThat(errContent.toString()).isEmpty(); // No warnings
+ }
+
+ @Test
+ @ClearEnvironmentVariable(key = "JPM_CACHE")
+ void testGetCacheDirWithNoOptionAndNoEnvironmentVariable() {
+ Main.DepsMixin mixin = new Main.DepsMixin();
+ // Neither command line option nor environment variable is set
+
+ Path result = mixin.getCacheDir();
+
+ assertThat(result).isNull();
+ assertThat(errContent.toString()).isEmpty(); // No warnings
+ }
+
+ @Test
+ @SetEnvironmentVariable(key = "JPM_CACHE", value = "")
+ void testGetCacheDirWithEmptyEnvironmentVariable() {
+ Main.DepsMixin mixin = new Main.DepsMixin();
+
+ Path result = mixin.getCacheDir();
+
+ assertThat(result).isNull();
+ assertThat(errContent.toString()).isEmpty(); // No warnings
+ }
+
+ @Test
+ @ClearEnvironmentVariable(key = "JPM_CACHE")
+ void testGetCacheDirMultipleCalls() {
+ Main.DepsMixin mixin = new Main.DepsMixin();
+ mixin.cacheDir = tempCacheDir1;
+
+ // Call multiple times to ensure consistent behavior
+ Path result1 = mixin.getCacheDir();
+ Path result2 = mixin.getCacheDir();
+
+ assertThat(result1).isEqualTo(tempCacheDir1);
+ assertThat(result2).isEqualTo(tempCacheDir1);
+ assertThat(result1).isEqualTo(result2);
+ assertThat(errContent.toString()).isEmpty(); // No warnings
+ }
+}