Repository: pantsbuild/pants

Issue:
Generate a .idea compatible seed project
Follow up for https://github.com/pantsbuild/pants/pull/8676, since ipr is legacy version.


Discussion:

Resolving patch:
diff --git a/src/python/pants/backend/project_info/tasks/idea_plugin_gen.py b/src/python/pants/backend/project_info/tasks/idea_plugin_gen.py
--- a/src/python/pants/backend/project_info/tasks/idea_plugin_gen.py
+++ b/src/python/pants/backend/project_info/tasks/idea_plugin_gen.py
@@ -84,6 +84,9 @@ def register_options(cls, register):
       removal_hint="`idea-plugin` should always act transitively, which is the default. This option "
                    "will be going away to ensure that Pants always does the right thing.",
     )
+    register('--possible-paths', type=list, default=['/Applications/IntelliJ IDEA CE.app',
+                                                     '/Applications/IntelliJ IDEA.app'],
+             help='Sets the the list of paths for IntelliJ lookup.')
 
   @property
   def act_transitively(self):
@@ -96,13 +99,12 @@ def __init__(self, *args, **kwargs):
     self.open = self.get_options().open
 
     self.java_encoding = self.get_options().java_encoding
-    self.project_template = os.path.join(_TEMPLATE_BASEDIR,
-                                         'project-12.mustache')
-    self.workspace_template = os.path.join(_TEMPLATE_BASEDIR,
+    self.idea_modules_template = os.path.join(_TEMPLATE_BASEDIR,
+                                           'modules-12.mustache')
+    self.idea_workspace_template = os.path.join(_TEMPLATE_BASEDIR,
                                            'workspace-12.mustache')
-    self.rootmodule_template = os.path.join(_TEMPLATE_BASEDIR,
-                                           'rootmodule-12.mustache')
     self.java_language_level = self.get_options().java_language_level
+    self.possible_paths = self.get_options().possible_paths
 
     if self.get_options().java_jdk_name:
       self.java_jdk = self.get_options().java_jdk_name
@@ -113,16 +115,13 @@ def __init__(self, *args, **kwargs):
     safe_mkdir(output_dir)
 
     with temporary_dir(root_dir=output_dir, cleanup=False) as output_project_dir:
-      project_name = self.get_project_name(self.context.options.specs)
-
       self.gen_project_workdir = output_project_dir
-      self.project_filename = os.path.join(self.gen_project_workdir,
-                                           '{}.ipr'.format(project_name))
-      self.workspace_filename = os.path.join(self.gen_project_workdir,
-                                             '{}.iws'.format(project_name))
-      self.rootmodule_filename = os.path.join(self.gen_project_workdir,
-                                              'rootmodule.iml')
+      self.idea_workspace_filename = os.path.join(self.gen_project_workdir,
+                                                  ".idea", "workspace.xml")
+      self.idea_modules_filename = os.path.join(self.gen_project_workdir,
+                                                  ".idea", "modules.xml")
       self.intellij_output_dir = os.path.join(self.gen_project_workdir, 'out')
+      self.intellij_idea_dir = os.path.join(self.gen_project_workdir, '.idea')
 
   @classmethod
   def get_project_name(cls, target_specs):
@@ -160,21 +159,20 @@ def generate_project(self):
 
     # Generate (without merging in any extra components).
     safe_mkdir(os.path.abspath(self.intellij_output_dir))
+    safe_mkdir(os.path.abspath(self.intellij_idea_dir))
 
     def gen_file(template_file_name, **mustache_kwargs):
       return self._generate_to_tempfile(
         Generator(pkgutil.get_data(__name__, template_file_name).decode(), **mustache_kwargs)
       )
 
-    ipr = gen_file(self.project_template, project=configured_project)
-    iws = gen_file(self.workspace_template, workspace=configured_workspace)
-    iml_root = gen_file(self.workspace_template)
+    idea_ws = gen_file(self.idea_workspace_template, workspace=configured_workspace)
+    idea_modules = gen_file(self.idea_modules_template, project=configured_project)
 
-    shutil.move(ipr, self.project_filename)
-    shutil.move(iws, self.workspace_filename)
-    shutil.move(iml_root, self.rootmodule_filename)
+    shutil.move(idea_ws, self.idea_workspace_filename)
+    shutil.move(idea_modules, self.idea_modules_filename)
 
-    return self.project_filename
+    return self.gen_project_workdir
 
   def _generate_to_tempfile(self, generator):
     """Applies the specified generator to a temp file and returns the path to that file.
@@ -205,6 +203,6 @@ def console_output(self, _targets):
         subprocess.Popen([open_with, ide_file], stdout=null, stderr=null)
       else:
         try:
-          desktop.ui_open(ide_file)
+          desktop.idea_open(ide_file, self.possible_paths[::-1])
         except desktop.OpenError as e:
           raise TaskError(e)
diff --git a/src/python/pants/util/desktop.py b/src/python/pants/util/desktop.py
--- a/src/python/pants/util/desktop.py
+++ b/src/python/pants/util/desktop.py
@@ -1,20 +1,41 @@
 # Copyright 2016 Pants project contributors (see CONTRIBUTORS.md).
 # Licensed under the Apache License, Version 2.0 (see LICENSE).
 
+import os
 import subprocess
 from typing import Sequence
 
 from pants.util.osutil import get_os_name
 
 
+class IdeaNotFoundException(Exception):
+  """Could not find Idea executable"""
+
+
 class OpenError(Exception):
   """Indicates an error opening a file in a desktop application."""
 
 
+def _mac_open_with_idea(file_: str, lookup_paths: list) -> None:
+  ideaPath = next((path for path in lookup_paths if os.path.isdir(path)), None)
+  if ideaPath is not None:
+    subprocess.call(["open", "-a", ideaPath, file_])
+  else:
+    raise IdeaNotFoundException("Could not find Idea executable in the following locations:\n{}".format("\n".join(lookup_paths)))
+
+
 def _mac_open(files: Sequence[str]) -> None:
   subprocess.call(['open'] + list(files))
 
 
+def _linux_open_with_idea(file_: str, lookup_paths: list) -> None:
+  cmd = "idea"
+  if not _cmd_exists(cmd):
+    raise OpenError("The program '{}' isn't in your PATH. Please install and re-run this "
+                    "goal.".format(cmd))
+  subprocess.Popen(["nohup", cmd, file_])
+
+
 def _linux_open(files: Sequence[str]) -> None:
   cmd = "xdg-open"
   if not _cmd_exists(cmd):
@@ -36,6 +57,25 @@ def _cmd_exists(cmd: str) -> bool:
   'linux': _linux_open
 }
 
+_IDEA_BY_OS = {
+  'darwin': _mac_open_with_idea,
+  'linux': _linux_open_with_idea
+}
+
+
+def idea_open(file_: str, lookup_paths: list) -> None:
+  """Attempts to open the given files using the preferred desktop viewer or editor.
+
+  :raises :class:`OpenError`: if there is a problem opening any of the files.
+  """
+  if file_:
+    osname = get_os_name()
+    opener = _IDEA_BY_OS.get(osname)
+    if opener:
+      opener(file_, lookup_paths)
+    else:
+      raise OpenError('Open currently not supported for ' + osname)
+
 
 def ui_open(*files: str) -> None:
   """Attempts to open the given files using the preferred desktop viewer or editor.


Test patch:
diff --git a/tests/python/pants_test/backend/project_info/tasks/test_idea_plugin_integration.py b/tests/python/pants_test/backend/project_info/tasks/test_idea_plugin_integration.py
--- a/tests/python/pants_test/backend/project_info/tasks/test_idea_plugin_integration.py
+++ b/tests/python/pants_test/backend/project_info/tasks/test_idea_plugin_integration.py
@@ -5,7 +5,7 @@
 import os
 from xml.dom import minidom
 
-from pants.backend.project_info.tasks.idea_plugin_gen import IDEA_PLUGIN_VERSION, IdeaPluginGen
+from pants.backend.project_info.tasks.idea_plugin_gen import IDEA_PLUGIN_VERSION
 from pants.base.build_environment import get_buildroot
 from pants.base.cmd_line_spec_parser import CmdLineSpecParser
 from pants.base.specs import DescendantAddresses, SiblingAddresses, SingleAddress
@@ -18,11 +18,11 @@ def _do_check(self, project_dir_path, expected_project_path, expected_targets,
                 incremental_import=None):
     """Check to see that the project contains the expected source folders."""
 
-    iws_file = os.path.join(
-      project_dir_path, f'{IdeaPluginGen.get_project_name(expected_targets)}.iws'
+    workspace_file = os.path.join(
+      project_dir_path, ".idea", 'workspace.xml'
     )
-    self.assertTrue(os.path.exists(iws_file))
-    dom = minidom.parse(iws_file)
+    self.assertTrue(os.path.exists(workspace_file))
+    dom = minidom.parse(workspace_file)
     self.assertEqual(1, len(dom.getElementsByTagName("project")))
     project = dom.getElementsByTagName("project")[0]