gnodet commented on code in PR #2198:
URL: https://github.com/apache/maven-resolver/pull/2198#discussion_r4237887966


##########
maven-resolver-impl/src/main/java/org/eclipse/aether/internal/impl/DefaultInstaller.java:
##########
@@ -205,7 +209,26 @@ private void install(RepositorySystemSession session, 
RequestTrace trace, Artifa
                 throw new IllegalStateException("cannot install " + dstPath + 
" to same path");
             }
 
-            pathProcessor.copyWithTimestamp(srcPath, dstPath);
+            boolean hardLink = ConfigUtils.getBoolean(
+                    session,
+                    ConfigurationProperties.DEFAULT_INSTALLER_HARD_LINK,
+                    ConfigurationProperties.INSTALLER_HARD_LINK);
+            if (hardLink) {
+                try {
+                    Files.createDirectories(dstPath.getParent());
+                    if (Files.isDirectory(dstPath)) {
+                        throw new IOException("destination is a directory: " + 
dstPath);

Review Comment:
   🔴 **IOException guard is immediately swallowed by the enclosing catch** — 
`throw new IOException("destination is a directory: " + dstPath)` at line 220 
is inside the `try` block whose `catch` at line 224 catches `IOException`. The 
intentional guard is silently caught, causing a fallback to 
`copyWithTimestamp(srcPath, dstPath)` on a directory — which also fails, but 
with a confusing, less actionable error.
   
   Move the directory check *before* the `try` block so it can propagate 
properly:
   
   ```suggestion
                       if (Files.isDirectory(dstPath)) {
                           throw new IOException("destination is a directory: " 
+ dstPath);
                       }
                       try {
   ```
   
   Or alternatively, re-throw when the caught `IOException` was the one we 
threw intentionally (check `e.getMessage().startsWith("destination is a 
directory")` — brittle, so the first option is cleaner).



##########
maven-resolver-impl/src/main/java/org/eclipse/aether/internal/impl/DefaultInstaller.java:
##########
@@ -205,7 +209,26 @@ private void install(RepositorySystemSession session, 
RequestTrace trace, Artifa
                 throw new IllegalStateException("cannot install " + dstPath + 
" to same path");
             }
 
-            pathProcessor.copyWithTimestamp(srcPath, dstPath);
+            boolean hardLink = ConfigUtils.getBoolean(

Review Comment:
   💡 **`ConfigUtils.getBoolean()` called per-artifact** — for a multi-module 
build, this reads the same immutable session property on every artifact. 
Consider hoisting it to the outer `install(RepositorySystemSession session, 
InstallRequest request)` and passing the boolean down to the private method.



##########
maven-resolver-api/src/main/java/org/eclipse/aether/ConfigurationProperties.java:
##########
@@ -88,6 +88,13 @@ public final class ConfigurationProperties {
      */
     public static final String PREFIX_UTIL = PREFIX_AETHER + "util.";
 
+    /**
+     * Prefix for installer related configurations. <em>For internal use 
only.</em>
+     *
+     * @since 2.0.9

Review Comment:
   ⚠️ **Wrong `@since` version** — `2.0.9` but the current POM is 
`2.0.25-SNAPSHOT`. Same issue on the `INSTALLER_HARD_LINK` (line 715) and 
`DEFAULT_INSTALLER_HARD_LINK` (line 726) entries.



##########
maven-resolver-impl/src/test/java/org/eclipse/aether/internal/impl/DefaultInstallerTest.java:
##########
@@ -377,4 +379,47 @@ void testSetArtifactTimestamps() throws 
InstallationException {
                 localArtifactFile.lastModified(),
                 "artifact timestamp was not set to src file");
     }
+
+    @Test
+    void testHardLinkInstallCreatesHardLink() throws InstallationException, 
IOException {
+        // default: hardLink=true — installer attempts a hard link, falls back 
to copy on cross-device
+        request.addArtifact(artifact);
+        installer.install(session, request);
+
+        assertTrue(localArtifactFile.exists());
+        assertEquals("artifact", TestFileUtils.readString(localArtifactFile));
+
+        // On same-device filesystems, verify a hard link was created (same 
inode).
+        // On cross-device (common in CI containers where /tmp and project dir 
are on different mounts),
+        // Files.createLink throws IOException and the installer falls back to 
copy — both are correct.
+        Path srcPath = artifact.getFile().toPath();
+        Path dstPath = localArtifactFile.toPath();
+        try {
+            boolean sameDevice =
+                    Files.getAttribute(srcPath, 
"unix:dev").equals(Files.getAttribute(dstPath, "unix:dev"));
+            if (sameDevice) {
+                // hard link must have been created — same inode expected
+                assertEquals(
+                        Files.getAttribute(srcPath, "unix:ino"),
+                        Files.getAttribute(dstPath, "unix:ino"),
+                        "installed artifact should be a hard link to the 
source on same-device FS");
+            }
+            // cross-device: fallback to copy is correct — content already 
asserted above
+        } catch (UnsupportedOperationException | IllegalArgumentException 
ignored) {

Review Comment:
   💬 **Test degeneracy on cross-device CI** — on any CI where `/tmp` and the 
project directory are on different mounts (very common with containers), 
`Files.createLink` throws `IOException`, the installer falls back to copy, and 
this test reduces to content-equality only — which is indistinguishable from a 
plain copy. The hard-link path is effectively untested in CI.
   
   Consider adding a `@EnabledOnOs(OS.LINUX)` + a same-device `tmpfs` fixture 
that forces the same-device scenario, or at minimum add a log/counter spy to 
assert the hard-link branch was taken.



##########
maven-resolver-impl/src/test/java/org/eclipse/aether/internal/impl/DefaultInstallerTest.java:
##########
@@ -377,4 +379,47 @@ void testSetArtifactTimestamps() throws 
InstallationException {
                 localArtifactFile.lastModified(),
                 "artifact timestamp was not set to src file");
     }
+
+    @Test
+    void testHardLinkInstallCreatesHardLink() throws InstallationException, 
IOException {
+        // default: hardLink=true — installer attempts a hard link, falls back 
to copy on cross-device
+        request.addArtifact(artifact);
+        installer.install(session, request);
+
+        assertTrue(localArtifactFile.exists());
+        assertEquals("artifact", TestFileUtils.readString(localArtifactFile));
+
+        // On same-device filesystems, verify a hard link was created (same 
inode).
+        // On cross-device (common in CI containers where /tmp and project dir 
are on different mounts),
+        // Files.createLink throws IOException and the installer falls back to 
copy — both are correct.
+        Path srcPath = artifact.getFile().toPath();
+        Path dstPath = localArtifactFile.toPath();
+        try {
+            boolean sameDevice =
+                    Files.getAttribute(srcPath, 
"unix:dev").equals(Files.getAttribute(dstPath, "unix:dev"));
+            if (sameDevice) {
+                // hard link must have been created — same inode expected
+                assertEquals(
+                        Files.getAttribute(srcPath, "unix:ino"),
+                        Files.getAttribute(dstPath, "unix:ino"),
+                        "installed artifact should be a hard link to the 
source on same-device FS");
+            }
+            // cross-device: fallback to copy is correct — content already 
asserted above
+        } catch (UnsupportedOperationException | IllegalArgumentException 
ignored) {
+            // non-POSIX filesystem — inode/device check not available; 
content assertion is sufficient
+        }
+    }
+
+    @Test
+    void testHardLinkDisabledFallsThroughToCopy() throws 
InstallationException, IOException {
+        // set hardLink=false — installer must copy, not hard-link
+        session.setConfigProperty(ConfigurationProperties.INSTALLER_HARD_LINK, 
false);
+        request.addArtifact(artifact);
+        installer.install(session, request);
+
+        assertTrue(localArtifactFile.exists());
+        assertEquals("artifact", TestFileUtils.readString(localArtifactFile));
+        // With copy the content is correct; we cannot assert different inodes 
portably
+        // but we verify the install succeeds and produces the correct content
+    }

Review Comment:
   💬 **`testHardLinkDisabledFallsThroughToCopy` only asserts content, not 
behavior** — there is no assertion that `Files.createLink` was *not* called. A 
spy/mock on `Files.createLink` (or a counter on the fallback-copy path) would 
make the test actually verify the `hardLink=false` code path is taken.



-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: [email protected]

For queries about this service, please contact Infrastructure at:
[email protected]

Reply via email to