2018-07-22

1: Getting the Project Directory Path Without Letting the Symbolic Links Resolved, in Gradle

<The previous article in this series | The table of contents of this series |


Summary


Being Troubled by Gradle's Peremptorily Resolving Symbolic Links? This Is What I Have Found.

Topics


About: Gradle

Starting Context


*If the reader doesn't stand in 'Starting Context', he or she has four options: 1. curses the rude author, who arrogantly requires something from his lordship or her ladyship, but first fulfills the prerequisite (reluctantly, of course) and returns here; 2. curses the said author, but shows heavenly mercy of at least listening to the reasons why the senseless author is claiming that it is required; 3. curses the said author, ignores the nonsense that is 'Starting Context', pecks at the main body, and curses the incompetent author that something isn't understandable; 4. curses the said author, condemns the author, and leaves.
  • The reader has knowledge on what Gradle is.
  • The reader has basic knowledge on how to write Gradle build scripts.
*The knowledge that is the prerequisite to the knowledge listed above is implicitly presupposed, even if it isn't mentioned above. For example, one cannot know what UNO is (at least at the level cited above) without having basic knowledge of the object-oriented programming paradigm (because UNO is based on the object-oriented programming paradigm).

Target Context


*If the reader isn't enthusiastic about 'Target Context', he or she has three options: 1. scorns the inept author, who cannot exactly satisfy his lordship's or her ladyship's demands, but shows angelic patience of at least listening to the reasons why the nerdy author is claiming that it is beneficial; 2. scorns the said author, but search the main body for some possible fragments of useful information (grudgingly, of course); 3. scorns the said author, scoffs at the author, and leaves.
  • The reader will know a way to get the project directory path without letting the symbolic links resolved, in Gradle.

Introduction


'Orientation' is meant for the passersby who don't stand in 'Starting Context' and/or aren't enthusiastic about 'Target Context'. It doesn't contain any new technical information and can be skipped by anyone who doesn't need any orientation.


Stage Direction
Here are Hypothesizer 7, Objector 1A, and Objector 1B in front of a computer screen.


Orientation


Hypothesizer 7
In this article, we will investigate how to get the project directory path without letting the symbolic links resolved, in Gradle.

Objector 1A
Do you mean what I mean?

Hypothesizer 7
Sir, . . . what do you mean?

Objector 1A
Getting the project directory path as I specified it in my terminal, of course.

Hypothesizer 7
Well, . . . let us clarify the situation. Do you have a Gradle project directory that is symbolic-linked to a path?

Objector 1A
Not necessarily the project directory itself is symbolic-linked, but then, an ancestor directory is symbolic-linked to a path.

Hypothesizer 7
You are right. Anyway, are you treating the project directory as it exists at the path created by the symbolic link?

Objector 1A
Of course. That is the purpose of symbolic links, right?

Hypothesizer 7
Right, as far as I know. So, for example, you go to the path in your terminal and executes the 'gradle' command . . .

Objector 1A
For example yes. I can get the project directory by 'project.projectDir' in Gradle, of course, but you know what? That doesn't return the path I naturally expect, but the the 'symbolic links'-resolved path!

Objector 1B
What's wrong with that? Gradle has kindly and correctly resolved the path for you.

Objector 1A
Gratuitously and inappropriately, I would say, lady. Who wants such behavior? For what I have created the symbolic link, do they think?

Objector 1B
What's the harm? As the resolved path is correct, you can happily access the directory, can't you?

Objector 1A
That isn't so. That resolved path is a problem because that breaks relative locations of things.

Objector 1B
. . . It is your fault that you use relative paths: you can use absolute paths.

Objector 1A
. . . Lady, since when has using relative paths become "a fault"? I use relative paths because they are convenient for users.

Objector 1B
What do you mean by "users"?

Objector 1A
I distribute my projects, which users can locate anywhere in their computers. I certainly and reasonably require relative paths of things to be preserved, but, you know, I'm not rude as to demand users to use specific directories, of course.

Objector 1B
. . . It will be OK if the relative locations of the things in the resolved paths are preserved.

Objector 1A
If . . ., but why should I demand such an unreasonable thing? I am not rude as that . . .

Objector 1B
Anyway, you are talking about a rare case.

Objector 1A
So? Minorities don't matter? I am talking about meddling, without which, there would have been no trouble at all! If Gradle didn't meddle, I could resolve symbolic links whenever I wanted to, but with the path already resolved, I have no means to reconstruct the unresolved path!

Hypothesizer 7
Well, I do not know whether it is 'meddling' or symbolic links are unintentionally resolved because of an implementational reason, but the behavior is unnatural from the viewpoint of users and is troublesome for some people.

Objector 1A
Of course! I'm using a path, and why Gradle should change the path, unasked?

Hypothesizer 7
Actually, I guess that the case is the latter because I do not see the phenomenon in Windows.

Objector 1B
Huh? Then, why am I, a Windows user, reading this?

Hypothesizer 7
I do not know, honestly . . .

Objector 1B
Why didn't you say so at the beginning?

Hypothesizer 7
I thought that Windows users would not read this anyway because the title of this article would not concern them . . .

Objector 1B
I certainly won't read!

Stage Direction
Objector 1B leaves disgustedly.

Hypothesizer 7
Good bye, madam.


Main Body


1: What Didn't Work


Hypothesizer 7
I state in advance that I have not found any ideal solution, but a passable one, at least for me.

Objector 1A
What is that passable solution?

Hypothesizer 7
Before delving into that, I intend to describe what did not work.

Objector 1A
Who wants to listen to such a thing? Just tell me what works.

Hypothesizer 7
As mine is not an ideal solution, I think that some people may want information on which they can further investigate . . .

Objector 1A
They may, but I want the solution first.

Hypothesizer 7
Then, please go to the the next section.

Stage Direction
Objector 1A immediately jumps away.

Hypothesizer 7
Well, I have lost the audience, but will continue anyway as someone might watch this recording in the future.

I have tried some other properties ('project.buildDir', for example) than 'project.projectDir', in vain: symbolic links in all such properties seem to have been already resolved.

Objector 1A
Regrettably.

Hypothesizer 7
Oh, . . . I thought that you had gone to the next section . . .

Objector 1A
I had, and have come back.

Hypothesizer 7
Well, how did you manage to go to the future (I have not recorded the next section yet) and come back?

Objector 1A
You told me to go to the future!

Hypothesizer 7
. . . Certainly I did, carelessly, but that should not enable you to . . .

Objector 1A
Never mind . . .

Hypothesizer 7
. . . Then, I tried to get the current directory as the unresolved path.

Objector 1A
That is meaningless: I cannot use the current directory path because the current directory may not equal the project directory.

Hypothesizer 7
I know. I just thought that I might be able to somehow use the current directory in order to reconstruct the unresolved project directory path.

Objector 1A
I doubt that.

Hypothesizer 7
Your doubt noted, but I am going to talk about it because in fact, we will use the unresolved current directory path later.

Objector 1A
Hmm . . .

Hypothesizer 7
We can get the current directory path by 'System.getProperty ("user.dir")', but the symbolic links there have been already resolved.

Objector 1A
. . . Why does Gradle so determinedly want to resolve symbolic links?

Hypothesizer 7
Actually, that is not a matter of Gradle, but of Java.

Objector 1A
Certainly, that system property is of Java.

Hypothesizer 7
I tried to get the current directory path in another way like this.

@Java Source Code
                                    import java.nio.file.Paths;
import java.nio.file.LinkOption;

  Paths.get (".").toRealPath (LinkOption.NOFOLLOW_LINKS)
                                

Objector 1A
Getting the path from '.' specifying the 'LinkOption.NOFOLLOW_LINKS' flag, which means 'not resolving symbolic links' . . .. That should work.

Hypothesizer 7
But does not. '.' already seems to refer to the resolved path.

Objector 1A
They seem to desperately want to eliminate the traces of symbolic links . . .

Hypothesizer 7
Then, I thought that that handling of the current directory by Java might be the root cause. What if I set 'user.dir' by '-Duser.dir=$(pwd)'?

Objector 1A
What if?

Hypothesizer 7
Now I can get the unresolved current directory path, but that did not change Gradle property values.

Objector 1A
So, Gradle persistently resolves symbolic links . . .

Hypothesizer 7
Then, I thought, "What if I explicitly specify the project directory by the '-p' flag of the 'gradle' command even when that is unnecessary?" In fact, I tried this.

@bash Source Code
                                    gradle -p $(pwd)
                                

Objector 1A
Ah, that should work.

Hypothesizer 7
But does not.

Objector 1A
. . . How relentless they are . . .


2: A Solution


Hypothesizer 7
After all, what I have come up with is passing the unresolved project directory path as an original Java system property (in fact, it does not have to be a Java system property, but can be a Gradle property).

Objector 1A
. . . That is a very commonplace solution: of course, you can set anything into an original property.

Hypothesizer 7
For example, we will execute the gradle command like this.

@bash Source Code
                                    gradle -Dc_projectDirectoryPath=$(pwd)
                                

Or like this.

@bash Source Code
                                    gradle -p /a/b/c -Dc_projectDirectoryPath=/a/b/c
                                

Objector 1A
You know, specifying '/a/b/c' twice is cumbersome.

Hypothesizer 7
I agree, but we can create a shell script, which we can call like this.

@bash Source Code
                                    Gradle.sh -p /a/b/c
                                

Or just like this if the current directory is the project directory.

@bash Source Code
                                    Gradle.sh
                                

Objector 1A
You know, having to specify the absolute path is cumbersome.

Hypothesizer 7
I agree. So, we will allow this when the current directory is '/a/b'.

@bash Source Code
                                    Gradle.sh -p c
                                

Objector 1A
Well, . . . of course: the shell script can convert the relative path to the absolute path.

Hypothesizer 7
That is a way, certainly, but I have took another way, in which the shell script sets the current directory absolute path and the project directory relative path and a Gradle build script constructs the project directory absolute path from the two.

Objector 1A
Whatever.

Hypothesizer 7
The source of the shell script is like this.

@bash Source Code
                                    #!/bin/bash

declare -a l_commandLineArguments=()
declare l_projectDirectoryPathSettingFlag=false
declare l_projectDirectoryPath="$(pwd)"
declare -i l_commandLineArgumentIndex=4
for l_commandLineArgument in "$@"
do
 if [ "${l_commandLineArgument}" == -p ]
 then
  l_projectDirectoryPathSettingFlag=true
 else
  if [ "${l_projectDirectoryPathSettingFlag}" == true ]
  then
   l_projectDirectoryPath="${l_commandLineArgument}"
  else
   l_commandLineArguments["${l_commandLineArgumentIndex}"]="${l_commandLineArgument}"
   l_commandLineArgumentIndex=(${l_commandLineArgumentIndex}+1)
  fi
  l_projectDirectoryPathSettingFlag=false
 fi
done
l_commandLineArguments[0]="-Duser.dir=$(pwd)"
l_commandLineArguments[1]=-p
l_commandLineArguments[2]="${l_projectDirectoryPath}"
l_commandLineArguments[3]="-Dc_projectDirectoryPath=${l_projectDirectoryPath}"
set -x
gradle "${l_commandLineArguments[@]}"
                                

Objector 1A
Ah.

Hypothesizer 7
In our build script, we can get the project directory path like this.

@bash Source Code
                                    import java.io.File
import java.nio.file.LinkOption;
import java.nio.file.Paths;

ext ({
 // Property Names Start
 c_operatingSystemNamePropertyName = "os.name"
 c_currentDirectoryPathPropertyName = "user.dir"
 c_projectDirectoryPathPropertyName = "c_projectDirectoryPath"
 // Property Names End
 // Operating System Names Start
 c_operatingSystemNameLinux = "Linux"
 // Operating System Names End
 // Directories Separators Start
  if (System.properties [c_operatingSystemNamePropertyName].startsWith (c_operatingSystemNameLinux)) {
   c_operatingSystemDirectoriesSeparator = "/"
  }
  else {
   c_operatingSystemDirectoriesSeparator = "\\"
  }
  // Directories Separators End
 // Closures Part 1 Start
 getDirectoryOrFileNormalizedPathFromPath = {String a_directoryOrFilePath ->
  Paths.get (a_directoryOrFilePath).normalize ().toString ()
 }
 getDirectoryOrFileNormalizedAbsolutePathFromPathWithBaseSpecified = {String a_directoryOrFilePath, String a_baseDirectoryOrFilePath ->
  if (a_directoryOrFilePath == null) {
   null
  }
  else {
   def l_directoryOrFileAbsolutePath = null
   if (!(new File (a_directoryOrFilePath)).isAbsolute ()) {
    l_directoryOrFileAbsolutePath = "${a_baseDirectoryOrFilePath}${c_operatingSystemDirectoriesSeparator}${a_directoryOrFilePath}"
   }
   else {
    l_directoryOrFileAbsolutePath = a_directoryOrFilePath
   }
   getDirectoryOrFileNormalizedPathFromPath (l_directoryOrFileAbsolutePath)
  }
 }
 // Closures Part 1 End
 // Directory Paths Start
 c_currentDirectoryPath = getDirectoryOrFileNormalizedPathFromPath (System.getProperty (c_currentDirectoryPathPropertyName))
 c_projectDirectoryPath = getDirectoryOrFileNormalizedAbsolutePathFromPathWithBaseSpecified (System.getProperty (c_projectDirectoryPathPropertyName), c_currentDirectoryPath)
 if (c_projectDirectoryPath == null) {
  c_projectDirectoryPath = projectDir.getPath ()
  }
 // Directory Paths End
 // Closures Part 2 Start
 getDirectoryOrFileNormalizedAbsolutePathFromPath = {String a_directoryOrFilePath ->
  getDirectoryOrFileNormalizedAbsolutePathFromPathWithBaseSpecified (a_directoryOrFilePath, c_projectDirectoryPath)
 }
 // Closures Part 2 End
})
                                

Objector 1A
Um? . . . Hmm, you use 'Path.normalize ()' . . .

Hypothesizer 7
That is important: that method returns the normalized path without resolving the symbolic links.

Objector 1A
What do you mean by "normalized path"?

Hypothesizer 7
I mean '/a/b/c' for '/a/b/./c', '/a/b/d/../c', etc.. Note that you cannot use '/a/b/d/../c' as it is because it refers to '/e/c' when '/a/b/d' is a symbolic link to '/e/f'.

Objector 1A
There is no such a thing as '/e/c' . . .

Hypothesizer 7
So, you have to do the normalization.


3: Setting the Project Directory Path to the Sub Project


Objector 1A
Certainly I can get the project directory path for the main project that way, but how can I get the project directory path for the sub project?

Hypothesizer 7
If you are calling the sub project by a 'GradleBuild' task, . . .

Objector 1A
Of course, I am.

Hypothesizer 7
. . . then, you can set the Java system parameter for the sub project like this.

@ Source Code
                                    ext ({
 // Property Names Start
 c_isSubProjectPropertyName = "c_isSubProject"
 // Property Names End
 // Path Names Start
 c_parentDirectoryPathName = ".."
 // Path Names End
 // File Names Start
 c_gradleDefaultBuildScriptName = "build.gradle"
 // File Names End
 // Project Properties Start
 c_isSubProject = System.getProperty (c_isSubProjectPropertyName)
 if (c_isSubProject == null) {
  c_isSubProject = "false"
 }
 // Project Properties End
 // Task Name Prefixes Start
 c_referencedProjectTaskCallTaskNamePrefix = "i_callReferencedProject"
 // Task Name Prefixes End
 // Task Name Suffixes Start
 c_referencedProjectTaskCallTaskNameSuffix = "Task"
 // Task Name Suffixes End
 // In Task Name Directories Separator Start
 c_referencedProjectTaskCallTaskNameDirectoriesSeparator = "#"
 // In Task Name Directories Separator End
 // Task Names Start
 c_testTaskName = "i_testTask"
 // Task Names End
})

ext ({
 c_targetName = "theBiasPlanet.gradleTestsMainProject"
 c_referencedProjectDirectoryPaths = ["${c_parentDirectoryPathName}${c_operatingSystemDirectoriesSeparator}gradleTestsSubProject"]
})

defaultTasks (c_testTaskName)

task (c_testTaskName + "", type: DefaultTask, {
 println (String.format ("### In the task creation: project = %s, task = %s", c_targetName, c_testTaskName))

 doFirst ({
  println (String.format ("### In doFirst: project = %s, task = %s", c_targetName, c_testTaskName))
 })

 doLast ({
  println (String.format ("### In doLast: project = %s, task = %s", c_targetName, c_testTaskName))
 })
})

if (!c_isSubProject.equals ("true")) {
 def Task l_lastReferencedProjectTask = null
 c_referencedProjectDirectoryPaths.each ({String a_referencedProjectDirectoryPath ->
  def String l_referencedProjectDirectoryAbsolutePath = getDirectoryOrFileNormalizedAbsolutePathFromPath (a_referencedProjectDirectoryPath)
  def Task l_referencedProjectTask = task (c_referencedProjectTaskCallTaskNamePrefix + a_referencedProjectDirectoryPath.replaceAll (c_operatingSystemDirectoriesSeparator, c_referencedProjectTaskCallTaskNameDirectoriesSeparator) + c_referencedProjectTaskCallTaskNameSuffix, type: GradleBuild, {
   buildFile = getDirectoryOrFileNormalizedAbsolutePathFromPathWithBaseSpecified (c_gradleDefaultBuildScriptName, l_referencedProjectDirectoryAbsolutePath)
   tasks = [c_testTaskName]
   startParameter.setSystemPropertiesArgs ([(c_currentDirectoryPathPropertyName + ""): c_currentDirectoryPath, (c_projectDirectoryPathPropertyName + ""): l_referencedProjectDirectoryAbsolutePath, (c_isSubProjectPropertyName + ""): "true"])
  })
  if (l_lastReferencedProjectTask != null) {
   l_referencedProjectTask.mustRunAfter (l_lastReferencedProjectTask)
  }
  tasks [c_testTaskName].dependsOn += l_referencedProjectTask
  l_lastReferencedProjectTask = l_referencedProjectTask
 })
 if (l_lastReferencedProjectTask != null) {
  tasks [c_testTaskName].mustRunAfter (l_lastReferencedProjectTask)
 }
}
                                

Objector 1A
Huh?

Hypothesizer 7
There is a sub project at '../gradleTestsSubProject' relative to this main project, as set in 'c_referencedProjectDirectoryPaths'; the sub project has a task named 'i_testTask', as set in 'c_testTaskName', which this main project executes before a task (also named 'i_testTask') of this main project is executed.

Objector 1A
What the hell is 'c_isSubProject'?

Hypothesizer 7
That the hell is a flag that prevents the sub project from executing its sub projects. I have implemented the flag because otherwise, some sub sub projects could be needlessly executed multiple times, being called by multiple sub projects.

Objector 1A
. . . Anyway, your code is weird. Why is the task name a 'String'? Why is '{}' inside '()'?

Hypothesizer 7
I write Gradle scripts in that style because it is more reasonable, more understandable, and of wider application.

Objector 1A
I don't understand at all.

Hypothesizer 7
We will address it in a future article. The coding style has nothing to do with the issue of this article, and you can rewrite the code as you like.


4: The Conclusion and Beyond


Hypothesizer 7
Now, we can get the project directory path without letting the symbolic links resolved, somehow.

Objector 1A
Wait! Something is wrong!

Hypothesizer 7
What is wrong with you, sir?

Objector 1A
It is not I that is wrong, but the symbolic link!

Hypothesizer 7
What is wrong with the symbolic link, then?

Objector 1A
While '/a/b/d' is a symbolic link to '/e/f' and '/a/b/c' is a proper directory, 'ls /a/b/d/../c' says that there is no such a directory; 'more /a/b/d/../c/build.gradle' says that there is no such a file . . .

Hypothesizer 7
Ah, apparently, those programs do not normalize the path first, but immediately resolves each symbolic link as they find one as they trace the path from the root directory, which causes an unnatural phenomenon that '/a/b/d/../c' does not exist while '/a/b/c' exists.

Objector 1A
What can I do about it?

Hypothesizer 7
Well, as it is a matter of those programs, what I come up with is only to create a shell script that normalizes paths for each program as a preprocessor of arguments, unless you are going to modify those programs.

Objector 1A
Hmm . . .


References


    <The previous article in this series | The table of contents of this series |