diff --git a/src/main/java/rife/bld/BaseProject.java b/src/main/java/rife/bld/BaseProject.java index 19c4839..042ae23 100644 --- a/src/main/java/rife/bld/BaseProject.java +++ b/src/main/java/rife/bld/BaseProject.java @@ -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 { *

* 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. + *

+ * 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. + *

+ * 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 { *

* 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. + *

+ * 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. + *

+ * 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 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()); } } diff --git a/src/test/java/rife/bld/TestBaseProject.java b/src/test/java/rife/bld/TestBaseProject.java new file mode 100644 index 0000000..263861b --- /dev/null +++ b/src/test/java/rife/bld/TestBaseProject.java @@ -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 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(); + 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(); + 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(); + 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(); + 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(); + 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(); + 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") + ); + } + } +}