Affected version
3.6.2, 3.5.0, 3.4.1
Bug description
Summary
RequireFilesExist reports an existing, readable, non-symlink file as missing when the configured path contains .. segments that traverse a firmlinked directory on macOS (e.g. /Users). The rule's symlink heuristic calls Path.toRealPath(), which throws on such paths; the resulting exception is caught and the file is conservatively classified as a symlink, causing the rule to fail.
Affected versions
Reproduced with maven-enforcer-plugin:
Environment
- OS: macOS (Darwin 25.3.0, APFS,
/Users is a firmlinked directory)
- Architecture: aarch64 (Apple Silicon)
- JDK: project's
${maven.compiler.release} (reproduced under standard Temurin JDK)
- Maven: 3.9.x
Steps to reproduce
-
On macOS, place a file at /Users/<user>/projects/repo/README.adoc.
-
From a Maven module at /Users/<user>/projects/repo/src/a/b/pom.xml, configure:
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-enforcer-plugin</artifactId>
<version>3.6.2</version>
<executions>
<execution>
<id>require-readme</id>
<phase>validate</phase>
<goals><goal>enforce</goal></goals>
<configuration>
<rules>
<requireFilesExist>
<files>
<file>${project.basedir}/../../../README.adoc</file>
</files>
</requireFilesExist>
</rules>
</configuration>
</execution>
</executions>
</plugin>
-
Run mvn enforcer:enforce@require-readme.
Expected
Build succeeds. The path resolves to an existing regular file (verifiable via ls, [ -f ... ], Files.exists(), and File.exists()).
Actual
[ERROR] Rule 0: org.apache.maven.enforcer.rules.files.RequireFilesExist failed with message:
[ERROR] Some required files are missing:
[ERROR] /Users/<user>/projects/repo/src/a/b/../../../README.adoc
The same rule succeeds when the <file> value is the canonical path with .. collapsed (/Users/<user>/projects/repo/README.adoc).
Root cause
In enforcer-rules, RequireFilesExist treats files as missing if they are symlinks:
@Override
boolean checkFile(File file) {
if (file == null) {
return true;
}
return file.exists() && !isSymlink(file);
}
private static boolean isSymlink(File file) {
try {
return Files.isSymbolicLink(file.toPath())
|| !Files.isSameFile(file.toPath(), file.toPath().toRealPath());
} catch (Exception e) {
return true; // ← any exception is treated as "this is a symlink"
}
}
On macOS, Path.toRealPath() can throw IOException (or a JDK-internal exception) when the input path traverses firmlinked system directories such as /Users via .. segments, even when the file itself exists, is a regular file, and is not a symlink. The catch-all then classifies the file as a symlink and the rule rejects it.
This is reproducible with a tiny Java snippet:
Path p = Paths.get("/Users/<user>/projects/repo/src/a/b/../../../README.adoc");
System.out.println(Files.exists(p)); // true
System.out.println(p.toFile().exists()); // true
System.out.println(Files.isSymbolicLink(p));// false
System.out.println(p.toRealPath()); // throws on affected macOS setups
Impact
Any multi-module project on macOS that uses RequireFilesExist with a path expressed relative to ${project.basedir} (e.g. checking a file at the repository root from a nested module) will get a spurious build failure. There is no diagnostic clue: the message says the file is missing, but it is not.
Suggested fixes
Any one of the following would resolve the issue:
- Drop the
toRealPath()-based symlink check from RequireFilesExist. RequireFilesExist's contract is "this file must exist" — symlink-vs-regular-file is arguably out of scope. If symlink rejection is required, expose it as an opt-in property.
- Use
Files.isSymbolicLink(path) only. Treating the file as a symlink only when Files.isSymbolicLink() returns true is sufficient for the documented contract and avoids toRealPath() failures on ..-containing paths entirely.
- Normalize the path before the symlink check. Calling
path.toAbsolutePath().normalize() before toRealPath() collapses .. segments and avoids the macOS-specific throw.
- Stop swallowing exceptions as "symlink". If
toRealPath() is retained, an IOException should not silently flip the verdict to "missing"; either fail with the underlying error or fall back to Files.isSymbolicLink().
Workaround
Replace the rule with maven-antrun-plugin, which uses Ant's <available> and resolves .. correctly:
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-antrun-plugin</artifactId>
<executions>
<execution>
<id>require-readme</id>
<phase>validate</phase>
<goals><goal>run</goal></goals>
<configuration>
<target>
<fail message="Required file not found: ${project.basedir}/../../../README.adoc">
<condition>
<not>
<available file="${project.basedir}/../../../README.adoc"/>
</not>
</condition>
</fail>
</target>
</configuration>
</execution>
</executions>
</plugin>
Affected version
3.6.2, 3.5.0, 3.4.1
Bug description
Summary
RequireFilesExistreports an existing, readable, non-symlink file as missing when the configured path contains..segments that traverse a firmlinked directory on macOS (e.g./Users). The rule's symlink heuristic callsPath.toRealPath(), which throws on such paths; the resulting exception is caught and the file is conservatively classified as a symlink, causing the rule to fail.Affected versions
Reproduced with
maven-enforcer-plugin:Environment
/Usersis a firmlinked directory)${maven.compiler.release}(reproduced under standard Temurin JDK)Steps to reproduce
On macOS, place a file at
/Users/<user>/projects/repo/README.adoc.From a Maven module at
/Users/<user>/projects/repo/src/a/b/pom.xml, configure:Run
mvn enforcer:enforce@require-readme.Expected
Build succeeds. The path resolves to an existing regular file (verifiable via
ls,[ -f ... ],Files.exists(), andFile.exists()).Actual
The same rule succeeds when the
<file>value is the canonical path with..collapsed (/Users/<user>/projects/repo/README.adoc).Root cause
In
enforcer-rules,RequireFilesExisttreats files as missing if they are symlinks:On macOS,
Path.toRealPath()can throwIOException(or a JDK-internal exception) when the input path traverses firmlinked system directories such as/Usersvia..segments, even when the file itself exists, is a regular file, and is not a symlink. The catch-all then classifies the file as a symlink and the rule rejects it.This is reproducible with a tiny Java snippet:
Impact
Any multi-module project on macOS that uses
RequireFilesExistwith a path expressed relative to${project.basedir}(e.g. checking a file at the repository root from a nested module) will get a spurious build failure. There is no diagnostic clue: the message says the file is missing, but it is not.Suggested fixes
Any one of the following would resolve the issue:
toRealPath()-based symlink check fromRequireFilesExist.RequireFilesExist's contract is "this file must exist" — symlink-vs-regular-file is arguably out of scope. If symlink rejection is required, expose it as an opt-in property.Files.isSymbolicLink(path)only. Treating the file as a symlink only whenFiles.isSymbolicLink()returnstrueis sufficient for the documented contract and avoidstoRealPath()failures on..-containing paths entirely.path.toAbsolutePath().normalize()beforetoRealPath()collapses..segments and avoids the macOS-specific throw.toRealPath()is retained, anIOExceptionshould not silently flip the verdict to "missing"; either fail with the underlying error or fall back toFiles.isSymbolicLink().Workaround
Replace the rule with
maven-antrun-plugin, which uses Ant's<available>and resolves..correctly: