Thursday, July 24, 2014

Eclipse Team Projects & Best Practices

Sometimes the metadata attached to an Eclipse project contain absolute pathnames to files on the local computer. That becomes an issue when the project is moved to a different location, to another computer or even on the same computer. It happens almost all the time with Java projects but potentially any Eclipse project has this issue. It is usually introduced by plugins that the Eclipse team has no control over, even though there are provisions for them writing relative pathnames. For example the Grails plugin always writes absolute pathnames into the file .project, even for files inside the project. What's up with that?

When absolute pathnames are included in a source code management (SCM) repository, such as Subversion or Git, then two serious problems occur. First when the project is checked out by another team member the first user's settings in the project do not correspond to the location of resources on the new computer, so the second user has to fix all of that. Even more important, when the second user checks the project back in and the first user checks it out the changes then first user's settings are all out of whack and have to be fixed again. Then the cycle of checking out new stuff and fixing project settings goes on forever.

A typical approach to handling these problems is to configure the SCM to ignore the .classpath and .project files, and all the files in the .settings folder. That is a simple approach, but it is not necessarily the best approach. So what exactly are the best practices in a team environment? After we investigate those we will look at step-by-step scenarios for Subversion and Git.

What is the Project Metadata?

There are three pieces to the metadata puzzle: the .classpath file, the .project files, and the files in the .settings folder. These files contain information that defines how the project is set up and how it should be built. When managed appropriately keeping the .classpath and .settings files in sync across a team is not a particular problem, but be warned that the .project file introduces some difficulties. The following three minor topics are snapshots of how these files are used:

.classpath

In the Java environment the CLASSPATH variable defines to the compiler and the JVM where to look for class and jar files when they are used by the application. The .classpath file is an XML document that Eclipse creates to manage where these resources for a project are located. Part of this information is used by Eclipse to figure out what to compile, and part is used to build the CLASSPATH variable that the compiler needs to find everything. The .classpath file is located in the project root.

When classes and jar files are kept in the project itself, for example in a lib directory, and added to the build path then Eclipse uses relative pathnames. The project may be moved to a new location without requiring any changes to the .classpath file. A Maven project dynamically loads the libraries necessary into the project and the same effect is achieved.

<?xml version="1.0" encoding="UTF-8"?>
<classpath>
@tab;<classpathentry kind="src" path="src"/>
@tab;<classpathentry kind="con" path="org.eclipse.jdt.launching.JRE_CONTAINER/org.eclipse.jdt.internal.debug.ui.launcher.StandardVMType/JavaSE-1.7"/>
@tab;<classpathentry kind="output" path="bin"/>
</classpath>


The typical .classpath file in figure one causes eclipse defines to Eclipse a source directory "src", a container with other required classes "con", and where the compiled classes will be placed: "bin."

Projects that depend on one another must be loaded into the same workspace. The pathnames in .classpath are relative to the "root" of the workspace, not where the projects are physically located:

<classpathentry kind="src" combinaccessrules="false" path="/ProjectB"/>


But it makes sense to place related projects together in the same root folder even if Eclipse does not need that. When the projects are kept together they are easier to manage.


.session/*

Initially you may not find a .session folder in the project directory. Files in this folder represent Eclipse settings for the project when those settings differ from the workspace settings. Eclipse will create this folder in the project root when the first project setting is established.

Here is an example: open the properties for a Java project. Navigate to Java Compiler -> Errors/Warnings. Check the box "Enable project specific settings" and change the setting for "Non-static access to a static member" from "Warning" to "Error." You will discover that Eclipse writes a .settings/org.eclipse.jdt.core.prefs file that contains settings specific to this project:

eclipse.preferences.version=1
...
org.eclipse.jdt.core.compiler.problem.staticAccessReceiver=error

Unfortunately the files in .settings are somewhat schizophrenic. The setting we just changed is something that the programmer in the local environment wants to see. On the other hand try navigating to "Java Compiler" in the project properties (the branch itself, not any leafs). If you click on "Enable project specific settings" and change the "Compiler compliance level" that is a setting that needs to be present everywhere the project is cloned:

eclipse.preferences.version=1
org.eclipse.jdt.core.compiler.codegen.inlineJsrBytecode=enabled
org.eclipse.jdt.core.compiler.codegen.targetPlatform=1.7
org.eclipse.jdt.core.compiler.codegen.unusedLocal=preserve
org.eclipse.jdt.core.compiler.compliance=1.7
org.eclipse.jdt.core.compiler.debug.lineNumber=generate
org.eclipse.jdt.core.compiler.debug.localVariable=generate
org.eclipse.jdt.core.compiler.debug.sourceFile=generate
org.eclipse.jdt.core.compiler.problem.assertIdentifier=error
org.eclipse.jdt.core.compiler.problem.enumIdentifier=error
org.eclipse.jdt.core.compiler.source=1.7


I make this change all the time: my Eclipse runs on Java 8 but I have many projects that still need to compile and run under Java 7.

The files created in the .settings file depend on the module with project-specific settings. If you change the default environment that Grails launches for "Run on server" then an org.grails.ide.eclipse.core.prefs file will be created to hold that setting:


eclipse.preferences.version=1
org.grails.ide.eclipse.core.org.grails.ide.eclipse.runonserver.RunOnServerProperties.ENV=dev


.project

The .project file defines for Eclipse how the project is put together. A full rundown on the project metadata is at http://wiki.eclipse.org/Project_Management_Infrastructure/Project_Metadata. It is an XML file located in the project root. An empty version would look like:


<?xml version="1.0" encoding="UTF-8"?>
<projectDescription>
@tab;<name>Empty Project</name>
@tab;<comment></comment>
@tab;<projects>
@tab;</projects>
@tab;<buildSpec>
@tab;</buildSpec>
@tab;<natures>
@tab;</natures>
@lt;/projectDescription>


Our problem is that Eclipse plugins also write sections into this file to save their own information. In figure seven for example, the Grails plugin writes a <linkedResources> section, defines a virtual folder named .link_to_grails_resources, and records the location of where the plugins actually are in the project. Unfortunately that plugin breaks the Eclipse guidelines and writes an absolute pathname from the root of the file system:

<linkedResources><br/> @tab;<link><br/> @tab;@tab;<name>.link_to_grails_plugins</name><br/> @tab;@tab;<type>2</type><br/> @tab;@tab;<location>/Users/user/git/ProjectA/target/work/plugins</location><br/> @tab;</link><br/> </linkedResources><br/>

If the folder containing the project moves locally or to a remote computer, that pathname will be broken.

So Don't Push the Metadata!

Blocking the metadata files from being committed to the SCM server is a common, easy solution to this problem. Well, it is easy to implement but maybe not quite so simple down the road. Here is a scenario that I have taught in many bootcamps:

  1. Configure the SCM to ignore .classpath, .project, and .settings.
  2. Commit the project to SCM.
  3. On another computer get the project from SCM.
  4. You cannot import the project as an "existing project" because Eclipse does not recognize it as a project. But Eclipse offers two solutions: import the project using the new project wizard, or import it as a general project and then convert it.

It is step four that makes things complicated. When you import a project from Subversion using the new project wizard then Eclipse actually builds a new project, then layers the old files over the top of it, and finally adds the connection back to SCM so that you can perform commits. The first problem is that unless it is a Maven project you have a new .classpath file, but that .classpath doesn't include any of the project libraries so you have to go fix that up. There will probably be other project settings out of whack too: project dependencies (in the .classpath again), and Java compiler level settings in the .settings folder. This technique is not always friendly to SCM software; Git is one of those applications.

When you clone a remote repository in Git, it creates a new local copy of the repository so the working files are already in place. When you try to import the project it has to use the folder where the working files already are. But when you use the new project wizard that writes over the existing source strange and wondrous things start to happen. Which means that it does not work right at all.

So what about importing it as a general project and then converting it? That does not overwrite any existing files, all it does is add a .project file. It does work for Java, but you are still in the same position (or worse) than with the new project wizard: your libraries (and possibly more) are out of whack and you have to struggle to fix all of that. And, I have tried but have never been successful importing a Grails project as a general project and then converting back into a Grails project.

Whatever can we do?

So what exactly are the problems that we identified? The .classpath file could contain absolute pathnames to things outside of the project. The .project file may be written by plugins with absolute pathnames. And the .settings files may contain some personal preferences alongside of things needed to make the project work. So what can we do to address those problems?

Best Practices

  1. Always use a root folder to define a project, and put the project in its own sub-folder. That leaves room in the parent folder for adding new, related projects. We will talk more about multi-project environments in a later section.
  2. Never link to a resource that is outside of the project folder. The exception is you can link to another project in the same parent folder. This makes the .classpath portable and now it may be committed to the repository.
  3. Use Maven or something that extends it, like Gradle, to manage the project. Resources are delivered dynamically and most of the problems with .classpath go away.
  4. Never set personal preferences in the project properties. Always set preferences at the workspace level, after all you want to see them in all projects. Use project settings only for things that affect how the project is built, such as the level of the Java compiler. Then the files in the .settings folder are portable.
  5. Commit the .classpath and .settings/* files. They will help when we import the project on another computer to make sure that everything is set up correctly.
  6. Never commit the .project file, we cannot control what the plugins add to it.
  7. Never leave a folder empty.
  8. Never commit any files or folders that can be recreated. Class files in Java, or the "bin" folder in an Eclipse Java project are good examples of this.

The last two sections of this article will address specific scenarios of how to accomplish committing and cloning a project successfully with Subversion and Git. Right now we want to look at the how of three of these best practices:

Always use a root (parent) folder to define a project...

The logic of keeping related projects together is simple: it is easier to manage them when they are in one place. Where to put the projects is the question: in the workspace folder or in a separate folder?

The only real discussion of this topic centers around Git. The eGit wizard warns you against creating Git repositories in a project in the workspace folder. And if you change the repository location in the wizard, then it physically moves the project to the new location. Watch out if you let the wizard move a project: some settings may be impacted by this move, which is the same problem that is the focus of this whole document.

So why does eGit have a problem with creating repositories in the workspace folder? There are not any technical reasons why a Git repository cannot be in a workspace folder. The warning addresses using the workspace folder itself as the location of the Git repository and having multiple projects in the workspace. Then all of the projects defined in the workspace folder become part of the Git repository. A common mistake is to create new projects in the workspace that were never intended to be added to the repository, but are added anyways.

So what about repositories with multiple projects? It seems to be a common occurrence for people to set up Git repositories with multiple projects and work on them in Eclipse. But we do not recommend that, and shortly we will discuss why. Keep in mind that managing multiple projects in a folder and a multi-project repository are not the same thing.

Never link to a resource that is outside of the project folder

This is actually quite easy to comply with. If you must include a link to an external resource during the build of a project, then copy that resource into a project directory and link to it there. Yeah, yeah, then multiple projects cannot share one copy of a resource and that is duplication. But presented with the choice of duplication over project portability in a team environment? I choose portability.

Honestly, it also makes it easier to manage the other projects. What if you want to upgrade a library that several projects are sharing? So you upgrade it and four projects build but the fifth fails because of a conflict. It is a whole lot easier to manage these dependencies on a project-by-project basis.

Never commit the .project file

So what about that .project file? Unfortunately we absolutely have to have it on the destination computer or we will not be able to import the project! The solution is simple:

Copy the .project file to .project.base in the project directory. Do not remove the original. The original will not get committed if we have the filters set right for the SCM, but the copy will be committed.

On the destination computer, after we pull the project from the SCM we need to copy the .project.base back to the missing .project file. Edit the .project file and look for the absolute pathnames. They will point to files in the project directory, but they are absolute for the source location. Fix them so they direct Eclipse to the new, correct location of the project.

That last step does introduce one annoying problem for us: if the clone and import are one step as with Subversion, then we have to interrupt the clone before we do the import. This means stepping outside of Eclipse to copy and fix the file, then going back to Eclipse to import the existing project. When the repository is Subversion then there will be an extra step to link the project back to the repository, which we will discuss in the Subversion steps farther on in this document.

Never leave a folder empty: gotcha!

The SCM applications will usually not store any information about an empty folder in the source. So when you clone it to a destination computer they will all disappear!

Here is a live example: when you build an empty Grails application it creates domain, controller, and view folders for model-view-controller (MVC) in the new application. It needs those folders, if they are not there Grails will not build the application. But if you simply commit the project to SCM and then fetch it, they are gone! So when you import the project without those folders it is broken and you'll have to find everything wrong and fix it.

The best solution for this problem is to create a dummy file in each empty directory to force it to be committed to the repository. Git has a built-in file that is useful for this: .gitignore. We could use a file of any name to make sure the folder is committed, but I would suggest putting a period in front of the name so it normally stays hidden in the Eclipse display.

Multi-Project Environments

We have already decided that we need to keep related projects together in the same root folder so that they are easier to manage. And they all need to be in the same workspace (virtually, not physically) when one project depends on another. But should the projects be committed to the same SCM repository?

One school of thought says yes, because then we are sure that everything is always committed and pulled together. There are some arguments against this though:

  • All of the projects are versioned, even if only one changes.
  • What if several projects share an individual project but they have no relationship to each other?
  • One project may need to depend on an older version of another project until future changes are implemented.
  • Eclipse does not handle multi-project repositories very well.

The last one is the kicker of course. In some situations you can get Eclipse to read multiple projects from a single repository; it kind-of does work with eGit. But you will have to jump through a whole lot of hoops to create new projects and add them to a single repository. Any perceived benefit is not worth the effort.

So the best practice is keep it simple: group your projects together in one place, import them into the same workspace when they depend on one another, and commit them separately to the SCM.


Creating and Importing Projects

Before we explore the two separate scenarios for Subversion and Git we need to understand how to get to some of the hidden files. When you have the package view open you are able to look at all of the files in a project (you cannot do that in the project view):



The first red circle in the figure highlights a pulldown menu available in the package view. The "Filters..." command, also circled in red, allows to to control what files we see. Uncheck the ".* resources" entry:



Now we can see the .settings folder and the .classpath and .project files in Eclipse: 



Creating and Importing Projects with the Subclipse plugin

Subversion is a shared repository on a server. Users checkout projects to work on, but there are no locks preventing multiple people from checking out the same code. When a programmer is finished they commit their local copy back into the Subversion server, creating a new version of the project.

Create and share a project

  1. Create a root folder for your related projects. This may be the Eclipse workspace folder if you want.
  2. Create your project in Eclipse. If the project is not in the workspace you will have to specify the folder to put the project in under the root folder you created in step one. Note that you need to specify the project folder name because Eclipse does not create a folder when it creates the project this way.
  3. Copy the .project file to .project.base. You can do this in Eclipse in the Package view.
  4. Confirm that the "Ignored Resources" in Eclipse "Team" preferences allows .classpath and .settings to be committed (they are not there or checked), and .project will be ignored:

  1. Right-click on the project and use Team -> Share to create a Subversion repository for the project.
  2. Right-click on the project and use Team -> Commit to save the project in the repository.

Import a project

  1. Create a root directory for one or more projects. This can be the Eclipse workspace folder if you choose.
  2. In the Eclipse "SVN Repository Exploring" perspective browse to the project.
  3. Right-click on the project and use the "Export" command to pull the project (Import pushes to Subversion, export pulls from it). The destination folder should be be the root folder; Eclipse will automatically create the project folder with the name of the project. Do not use the "Checkout" command, that requires an immediate import of the project.
  4. On Windows in the Windows/File Explorer open the project folder and copy the .project.base file to ".project." Note that there is a trailing period; if you do not put the trailing period Windows will reject the filename. On Linux or the Mac use a corresponding tool or copy the file at the command line.
  5. Edit the .project file and fix any absolute pathnames that it contains.
  6. Return to Eclipse, right-click on the project or package explorer and use the "Import..." command to import the existing project into the workspace.
  7. Right-click on the project and use Team->Share to connect the project to the original repository location. Make sure that the pathname is correct; Eclipse may double-up the project name at the end of the path. Eclipse will notice that the project exists in the repository and tell you that it has to copy it into your project directory. Choose OK, these are the same files that you just fetched.

Later on if any changes are made that affect the .project file then copy it again to the .project.base before committing the project. Then communicate with the rest of the team members what has changed. They have to option of recreating the change in their environment or looking at the .project.base file to get the newest settings.

Project changes committed to the .classpath or .settings files should automatically be incorporated during the next update from the repository.

Creating and Importing Projects with the eGit plugin

Git is a distributed SCM: a remote repository is cloned completely to a local repository, and we work in the working directory of the local repository. After changes are committed to the local repository then they may be pushed up to a remote repository.

The steps here are built around the eGit plugin which works with all Git repositories, not the GitHub plugin that only communicates with their service. eGit will work with GitHub. However, if you use the GitHub plugin the steps are the same but the plug-in windows are different.

Create and share a project

  1. Create a root folder for your related projects. You may use the Eclipse workspace folder if you follow our one-project-per-repository advice (see the notes for the "Always use a root folder to define a project" in the best practices above). eGit will warn you about using the workspace.
  2. Create your project in Eclipse. You have to override the default location and specify the folder to put the project in under the root folder you created in step one. Note that you must specify the project folder name because Eclipse does not automatically create a corresponding folder when it creates the project.
  3. Copy the .project file to .project.base.
  4. Confirm that .classpath and .settings files will be committed, and the .project file will be ignored. eGit does not use the "Ignored Resources" in "Team" preferences; eGit uses the .gitignore file in the project directory and .gitignore_global in your home directory.
.git
.gitignore
.project
*.class
/bin/

  1. Right the project click and use Team -> Share Project to add a local Git repository to the project. Since we have decided that one project per repository is a best practice check "Use or create repository in parent folder of project," click on the project in the list, and finally on the Create Repository button. Then you will be able to click Finish and you will have the repository.

  1. Right click the project and use Team -> Add to Index to add the entire project to the Git index.
  2. Right click the project and use Team -> Commit to commit the staged files.

Import a project

  1. If you are only using a local git repository then where it is located is the root folder for projects; skip to step 5.
  2. Create a root folder for one or more projects. You may use the Eclipse workspace folder if you follow our one-project-per-repository advice (eGit will warn you too).
  3. In the "Git Repositories" view of the "Git" perspective click the button to "clone a Git repository."

  1. Clone the desired remote repository to the folder created in step 2. Give the wizard the details about the remote repository location. On the fourth page of the wizard where it asks for a destination directory make sure that you specify the project folder in the name, because eGit will not automatically create it:

  1. On Windows in the Windows/File Explorer open the project folder and copy the .project.base file to ".project." Note that there is a trailing period; if you do not put the trailing period Windows will reject the filename. On Linux or the Mac use a corresponding tool or copy the file at the command line.
  2. Edit the .project file and fix any absolute pathnames that it contains.
  3. Return to Eclipse and the Git perspective, right-click on the repository and use "Import Projects..." command to import the project into the workspace. Choose "Import existing projects" in the wizard and take the defaults on the following pages.
  4. The project will remain connected to the local Git repository, and to the remote repository if you cloned it from one.

Later on if any changes are made that affect the .project file then copy it again to the .project.base before committing the project. Then communicate with the rest of the team members what has changed. They have to option of recreating the change in their environment or looking at the .project.base file to get the newest settings.

Project changes committed to the .classpath or .settings files should automatically be incorporated during the next update from the repository.

Other stuff to ignore in SCM commits

Editors may sometimes create temporary files in a folder with the file being edited. VIM creates .swp files, other editors typically use the filename with a trailing tilde (~). Make sure that these files are ignored when the project is committed.

The compiled class files are another group of files that really should not be committed to a shared repository. Importing a class file will not affect another programmer. But class files are binary files and the changes between the files (deltas) cannot be calculated so on every commit. That means that a completely new copy of the file is added to the repository on every commit. Ignore *.class, and in many Eclipse projects you can ignore the /bin folder.

Wrap-up

Unfortunately there are just some limiting factors for sharing Eclipse projects in a team: programmer preferences and are mixed in the .settings files, there isn't a way to force project relative dependencies that make it into the .classpath file, and the .settings files contain a mix of personal preferences and project dependencies.

Through best-practices we eliminated problems with .classpath and .settings files. That just left the .project file, and we have no control over the plugins that may not follow the rules to the letter. So we offered a compromise so that projects may be committed and checked out, even though there are some difficulties if the project dependencies change. But hey, the problem of project properties changing existed in other solutions before we explored this path anyways! So good luck to you.



No comments:

Post a Comment