Add support for specifying a local JAR or module using an absolute path (#92)

This commit is contained in:
Erik C. Thauvin 2026-07-18 08:58:04 -04:00 committed by Geert Bevin
parent c005bf42d2
commit 5fac54ad16
2 changed files with 403 additions and 11 deletions

View file

@ -11,6 +11,8 @@ import rife.bld.operations.*;
import rife.tools.FileUtils;
import java.io.File;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.*;
import java.util.regex.Pattern;
@ -925,14 +927,40 @@ public class BaseProject extends BuildExecutor {
* <p>
* If the local dependency points to a directory, it will be scanned for jar files.
*
* @param path the file system path of the local dependency
* @param path the file system path (absolute or relative to the {@link #workDirectory})
* of the local dependency
* @since 1.5.2
*/
public LocalDependency local(String path) {
return new LocalDependency(path);
}
/**
* Creates a local dependency instance.
* <p>
* If the local dependency points to a directory, it will be scanned for jar files.
*
* @param path the file system path (absolute or relative to the {@link #workDirectory})
* of the local dependency
* @since 2.4.0
*/
public LocalDependency local(Path path) {
return new LocalDependency(path.toString());
}
/**
* Creates a local dependency instance.
* <p>
* If the local dependency points to a directory, it will be scanned for jar files.
*
* @param path the file system path (absolute or relative to the {@link #workDirectory})
* of the local dependency
* @since 2.4.0
*/
public LocalDependency local(File path) {
return new LocalDependency(path.getPath());
}
/**
* Creates a new module instance.
*
@ -1076,14 +1104,40 @@ public class BaseProject extends BuildExecutor {
* <p>
* If the local module points to a directory, it will be scanned for jar files.
*
* @param path the file system path of the local module
* @param path the file system path (absolute or relative to the {@link #workDirectory})
* of the local module
* @since 2.1
*/
public LocalModule localModule(String path) {
return new LocalModule(path);
}
/**
* Creates a local module instance.
* <p>
* If the local module points to a directory, it will be scanned for jar files.
*
* @param path the file system path (absolute or relative to the {@link #workDirectory})
* of the local module
* @since 2.4.0
*/
public LocalModule localModule(Path path) {
return new LocalModule(path.toString());
}
/**
* Creates a local module instance.
* <p>
* If the local module points to a directory, it will be scanned for jar files.
*
* @param path the file system path (absolute or relative to the {@link #workDirectory})
* of the local module
* @since 2.4.0
*/
public LocalModule localModule(File path) {
return new LocalModule(path.getPath());
}
/*
* Project directories
*/
@ -1942,14 +1996,25 @@ public class BaseProject extends BuildExecutor {
}
private void addLocalJars(List<File> jars, String path) {
var local_file = new File(workDirectory(), path);
if (local_file.exists()) {
if (local_file.isDirectory()) {
var local_jar_files = FileUtils.getFileList(local_file.getAbsoluteFile(), INCLUDED_JARS, EXCLUDED_JARS);
jars.addAll(new ArrayList<>(local_jar_files.stream().map(file -> new File(local_file, file)).toList()));
} else {
jars.add(local_file);
var local_path = Path.of(path);
if (!local_path.isAbsolute()) {
local_path = workDirectory().toPath().resolve(path);
}
if (!Files.exists(local_path)) {
if (verbose()) {
System.err.println("Invalid local dependency or module path, skipped: " + path);
}
return;
}
if (Files.isDirectory(local_path)) {
var local_jar_files = FileUtils.getFileList(local_path.toFile(), INCLUDED_JARS, EXCLUDED_JARS);
for (var jar : local_jar_files) {
jars.add(local_path.resolve(jar).toFile());
}
} else {
jars.add(local_path.toFile());
}
}

View file

@ -0,0 +1,327 @@
/*
* Copyright 2026 Erik C. Thauvin (https://erik.thauvin.net/)
* Licensed under the Apache License, Version 2.0 (the "License")
*/
package rife.bld;
import org.junit.jupiter.api.*;
import org.junit.jupiter.api.io.TempDir;
import rife.bld.dependencies.Scope;
import java.io.File;
import java.lang.reflect.Method;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.ArrayList;
import java.util.List;
import static org.junit.jupiter.api.Assertions.*;
class TestBaseProject {
private static Method ADD_LOCAL_JARS;
@TempDir
Path tempDir;
private BaseProject project;
@BeforeAll
static void initReflection() throws Exception {
ADD_LOCAL_JARS = BaseProject.class.getDeclaredMethod("addLocalJars", List.class, String.class);
ADD_LOCAL_JARS.setAccessible(true);
}
@BeforeEach
void beforeEach() {
project = new BaseProject();
project.workDirectory = tempDir.toFile();
project.createProjectStructure();
}
private void invokeAddLocalJars(List<File> jars, String path) throws Exception {
ADD_LOCAL_JARS.invoke(project, jars, path);
}
@Nested
@DisplayName("local(Path) and local(File)")
class LocalDependencyTest {
@Test
@DisplayName("local(Path) relative should resolve via workDirectory")
void localPathRelative() throws Exception {
var jar = Files.createFile(tempDir.resolve("a.jar"));
var dep = project.local(Path.of("a.jar"));
assertAll(
() -> assertNotNull(dep, "local(Path) should not return null"),
() -> assertEquals("a.jar", dep.path(), "relative Path should be kept as-is")
);
project.dependencies().scope(Scope.compile).include(dep);
assertTrue(project.compileClasspathJars().stream()
.anyMatch(f -> f.getAbsolutePath().equals(jar.toFile().getAbsolutePath())),
"compileClasspathJars should resolve relative path against workDirectory");
}
@Test
@DisplayName("local(Path) absolute should be kept absolute")
void localPathAbsolute() throws Exception {
var jar = Files.createFile(tempDir.resolve("abs.jar"));
var dep = project.local(jar);
assertAll(
() -> assertNotNull(dep, "local(Path) absolute should not return null"),
() -> assertEquals(jar.toString(), dep.path(), "absolute Path should be kept absolute")
);
project.dependencies().scope(Scope.compile).include(dep);
assertTrue(project.compileClasspathJars().stream()
.anyMatch(f -> f.getName().equals("abs.jar")),
"compileClasspathJars should include absolute Path dependency");
}
@Test
@DisplayName("local(File) relative file should be kept as-is")
void localFileRelative() {
var dep = project.local(new File("b.jar"));
assertAll(
() -> assertNotNull(dep, "local(File) should not return null"),
() -> assertEquals("b.jar", dep.path(), "relative File should be kept as-is")
);
}
@Test
@DisplayName("local(Path) absolute outside the project should resolve")
void localPathAbsoluteOutsideProject(@TempDir Path otherDir) throws Exception {
var jar = Files.createFile(otherDir.resolve("outside.jar"));
project.dependencies().scope(Scope.compile).include(project.local(jar));
assertTrue(project.compileClasspathJars().stream()
.anyMatch(f -> f.getAbsolutePath().equals(jar.toFile().getAbsolutePath())),
"compileClasspathJars should include an absolute path outside the project");
}
@Test
@DisplayName("local(File) absolute file")
void localFileAbsolute() throws Exception {
var jar = Files.createFile(tempDir.resolve("c.jar"));
var dep = project.local(jar.toFile());
assertAll(
() -> assertNotNull(dep, "local(File) absolute should not return null"),
() -> assertEquals(jar.toFile().getAbsolutePath(), dep.path(),
"absolute File should be kept absolute")
);
}
@Test
@DisplayName("local(File) directory should scan jars excluding sources/javadoc")
void localFileDirectory() throws Exception {
var dir = Files.createDirectory(tempDir.resolve("libs-dep-filter"));
Files.createFile(dir.resolve("one.jar"));
Files.createFile(dir.resolve("one-sources.jar"));
Files.createFile(dir.resolve("one-JAVADOC.jar"));
var dep = project.local(dir.toFile());
project.dependencies().scope(Scope.compile).include(dep);
var cp = project.compileClasspathJars();
assertAll(
() -> assertTrue(cp.stream().anyMatch(
f -> f.getName().equals("one.jar")), "regular jar should be included"),
() -> assertFalse(cp.stream().anyMatch(f -> f.getName().equals("one-sources.jar")),
"sources jar should be excluded (case-insensitive)"),
() -> assertFalse(cp.stream().anyMatch(
f -> f.getName().toLowerCase().contains("javadoc")),
"javadoc jar should be excluded")
);
}
@Test
@DisplayName("local(Path) and local(String) should behave same")
void parityWithStringOverload() throws Exception {
Files.createFile(tempDir.resolve("parity.jar"));
var fromString = project.local("parity.jar");
var fromPath = project.local(Path.of("parity.jar"));
var fromFile = project.local(new File("parity.jar"));
assertAll(
() -> assertEquals(fromString.path(), fromPath.path(),
"String and Path overloads should have same path()"),
() -> assertEquals(fromString.path(), fromFile.path(),
"String and File overloads should have same path()")
);
}
}
@Nested
@DisplayName("addLocalJars - private implementation")
class LocalJarsTest {
@Test
@DisplayName("should add relative file path")
void relativeFile() throws Exception {
Files.createFile(tempDir.resolve("my.jar"));
var jars = new ArrayList<File>();
invokeAddLocalJars(jars, "my.jar");
assertEquals(1, jars.size(), "relative file that exists should be added");
}
@Test
@DisplayName("should add absolute file path")
void absoluteFile() throws Exception {
var jar = Files.createFile(tempDir.resolve("abs2.jar"));
var jars = new ArrayList<File>();
invokeAddLocalJars(jars, jar.toAbsolutePath().toString());
assertEquals(1, jars.size(), "absolute file that exists should be added");
}
@Test
@DisplayName("should resolve relative to workDirectory")
void relativeToWorkDirectory() throws Exception {
var sub = Files.createDirectory(tempDir.resolve("sub"));
var jar = Files.createFile(sub.resolve("x.jar"));
var jars = new ArrayList<File>();
invokeAddLocalJars(jars, "sub/x.jar");
assertEquals(jar.toFile().getAbsoluteFile(), jars.get(0).getAbsoluteFile(),
"should resolve against workDirectory");
}
@Test
@DisplayName("should scan directory for jars only")
void directoryScansJars() throws Exception {
var libDir = Files.createDirectory(tempDir.resolve("libs-jars-scan"));
Files.createFile(libDir.resolve("a.jar"));
Files.createFile(libDir.resolve("b.jar"));
Files.createFile(libDir.resolve("not-a-jar.txt"));
var jars = new ArrayList<File>();
invokeAddLocalJars(jars, "libs-jars-scan");
assertEquals(2, jars.size(), "should only include .jar files");
}
@Test
@DisplayName("should exclude -sources and -javadoc jars in directory")
void directoryExcludesSourcesAndJavadoc() throws Exception {
var libDir = Files.createDirectory(tempDir.resolve("libs2-filter"));
Files.createFile(libDir.resolve("foo.jar"));
Files.createFile(libDir.resolve("foo-sources.jar"));
Files.createFile(libDir.resolve("foo-javadoc.jar"));
var jars = new ArrayList<File>();
invokeAddLocalJars(jars, "libs2-filter");
assertAll(
() -> assertEquals(1, jars.size(), "should filter out -sources and -javadoc"),
() -> assertEquals("foo.jar", jars.get(0).getName(), "only foo.jar should remain")
);
}
@Nested
@DisplayName("integration via public classpath methods")
class Integration {
@Test
@DisplayName("compileClasspathJars should include local file dependency")
void viaCompileClasspath() throws Exception {
var myJar = Files.createFile(tempDir.resolve("custom.jar"));
project.dependencies().scope(Scope.compile).include(project.local("custom.jar"));
var classpath = project.compileClasspathJars();
assertTrue(classpath.stream().anyMatch(
f -> f.getAbsolutePath().equals(myJar.toFile().getAbsolutePath())),
"compileClasspathJars should include local file dependency");
}
}
@Nested
@DisplayName("verbose branch")
class VerboseBranch {
@BeforeEach
void enableVerbose() {
project = new BaseProject() {
@Override
public boolean verbose() {
return true;
}
};
project.workDirectory = tempDir.toFile();
project.createProjectStructure();
}
@Test
@DisplayName("should skip non-existent path without throwing, even verbose")
void nonExistentPathSkipped() {
var jars = new ArrayList<File>();
assertDoesNotThrow(() -> invokeAddLocalJars(jars, "does-not-exist-verbose"),
"should not throw for missing path");
assertTrue(jars.isEmpty(), "non-existent path should be skipped");
}
}
}
@Nested
@DisplayName("localModule(Path) and localModule(File)")
class LocalModuleTest {
@Test
@DisplayName("localModule(Path) relative")
void localModulePathRelative() {
var mod = project.localModule(Path.of("mod.jar"));
assertAll(
() -> assertNotNull(mod, "should not return null"),
() -> assertEquals("mod.jar", mod.path(), "relative path kept as-is")
);
}
@Test
@DisplayName("localModule(Path) absolute")
void localModulePathAbsolute() throws Exception {
var jar = Files.createFile(tempDir.resolve("modAbs.jar"));
var mod = project.localModule(jar);
assertAll(
() -> assertNotNull(mod, "should not return null"),
() -> assertEquals(jar.toString(), mod.path(), "absolute path kept absolute")
);
}
@Test
@DisplayName("localModule(File) absolute")
void localModuleFileAbsolute() throws Exception {
var jar = Files.createFile(tempDir.resolve("mod2.jar"));
var mod = project.localModule(jar.toFile());
assertAll(
() -> assertNotNull(mod, "should not return null"),
() -> assertEquals(jar.toFile().getAbsolutePath(), mod.path(),
"absolute File should be kept absolute")
);
}
@Test
@DisplayName("localModule(File) directory scan")
void localModuleFileDirectory() throws Exception {
var dir = Files.createDirectory(tempDir.resolve("mods-filter"));
Files.createFile(dir.resolve("m1.jar"));
Files.createFile(dir.resolve("m1-javadoc.jar"));
var mod = project.localModule(dir.toFile());
project.dependencies().scope(Scope.compile).include(mod);
var mp = project.compileModulePathJars();
assertAll(
() -> assertTrue(mp.stream().anyMatch(f -> f.getName().equals("m1.jar")),
"regular jar should be in module path"),
() -> assertFalse(mp.stream().anyMatch(f -> f.getName().contains("javadoc")),
"javadoc jar should be excluded")
);
}
@Test
@DisplayName("all three overloads are consistent")
void overloadsConsistent() throws Exception {
Files.createFile(tempDir.resolve("cons.jar"));
var s = project.localModule("cons.jar");
var p = project.localModule(Path.of("cons.jar"));
var f = project.localModule(new File("cons.jar"));
assertAll(
() -> assertEquals(s.path(), p.path(), "String and Path overloads equal"),
() -> assertEquals(s.path(), f.path(), "String and File overloads equal")
);
}
}
}