Merge remote-tracking branch 'remotes/origin/master' into pycharm/docker
[idea/community.git] / python / src / com / jetbrains / python / sdk / PythonSdkType.java
1 /*
2  * Copyright 2000-2015 JetBrains s.r.o.
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  * http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 package com.jetbrains.python.sdk;
17
18 import com.google.common.collect.ImmutableMap;
19 import com.google.common.collect.Lists;
20 import com.intellij.execution.ExecutionException;
21 import com.intellij.execution.configurations.GeneralCommandLine;
22 import com.intellij.execution.process.ProcessOutput;
23 import com.intellij.facet.Facet;
24 import com.intellij.facet.FacetConfiguration;
25 import com.intellij.facet.FacetManager;
26 import com.intellij.ide.DataManager;
27 import com.intellij.notification.Notification;
28 import com.intellij.notification.NotificationListener;
29 import com.intellij.notification.NotificationType;
30 import com.intellij.notification.Notifications;
31 import com.intellij.openapi.actionSystem.CommonDataKeys;
32 import com.intellij.openapi.application.Application;
33 import com.intellij.openapi.application.ApplicationManager;
34 import com.intellij.openapi.application.ModalityState;
35 import com.intellij.openapi.application.PathManager;
36 import com.intellij.openapi.diagnostic.Logger;
37 import com.intellij.openapi.fileChooser.FileChooserDescriptor;
38 import com.intellij.openapi.module.Module;
39 import com.intellij.openapi.module.ModuleUtilCore;
40 import com.intellij.openapi.progress.ProgressIndicator;
41 import com.intellij.openapi.progress.ProgressManager;
42 import com.intellij.openapi.progress.Task;
43 import com.intellij.openapi.project.DumbModePermission;
44 import com.intellij.openapi.project.DumbService;
45 import com.intellij.openapi.project.Project;
46 import com.intellij.openapi.projectRoots.*;
47 import com.intellij.openapi.roots.ModuleRootManager;
48 import com.intellij.openapi.roots.OrderRootType;
49 import com.intellij.openapi.ui.Messages;
50 import com.intellij.openapi.util.*;
51 import com.intellij.openapi.util.io.FileSystemUtil;
52 import com.intellij.openapi.util.io.FileUtil;
53 import com.intellij.openapi.util.text.CharFilter;
54 import com.intellij.openapi.util.text.StringUtil;
55 import com.intellij.openapi.vfs.JarFileSystem;
56 import com.intellij.openapi.vfs.LocalFileSystem;
57 import com.intellij.openapi.vfs.VfsUtilCore;
58 import com.intellij.openapi.vfs.VirtualFile;
59 import com.intellij.psi.PsiElement;
60 import com.intellij.reference.SoftReference;
61 import com.intellij.remote.*;
62 import com.intellij.util.ArrayUtil;
63 import com.intellij.util.Consumer;
64 import com.intellij.util.NullableConsumer;
65 import com.intellij.util.ui.UIUtil;
66 import com.jetbrains.python.PyBundle;
67 import com.jetbrains.python.PyNames;
68 import com.jetbrains.python.PythonFileType;
69 import com.jetbrains.python.PythonHelper;
70 import com.jetbrains.python.codeInsight.userSkeletons.PyUserSkeletonsUtil;
71 import com.jetbrains.python.facet.PythonFacetSettings;
72 import com.jetbrains.python.packaging.PyCondaPackageManagerImpl;
73 import com.jetbrains.python.psi.LanguageLevel;
74 import com.jetbrains.python.psi.impl.PyBuiltinCache;
75 import com.jetbrains.python.psi.search.PyProjectScopeBuilder;
76 import com.jetbrains.python.remote.PyRemoteSdkAdditionalDataBase;
77 import com.jetbrains.python.remote.PythonRemoteInterpreterManager;
78 import com.jetbrains.python.sdk.flavors.CPythonSdkFlavor;
79 import com.jetbrains.python.sdk.flavors.PythonSdkFlavor;
80 import icons.PythonIcons;
81 import org.jdom.Element;
82 import org.jetbrains.annotations.NonNls;
83 import org.jetbrains.annotations.NotNull;
84 import org.jetbrains.annotations.Nullable;
85
86 import javax.swing.*;
87 import javax.swing.event.HyperlinkEvent;
88 import java.awt.*;
89 import java.io.File;
90 import java.io.IOException;
91 import java.lang.ref.WeakReference;
92 import java.util.*;
93 import java.util.List;
94 import java.util.regex.Pattern;
95
96 /**
97  * @author yole
98  */
99 public class PythonSdkType extends SdkType {
100   public static final String REMOTE_SOURCES_DIR_NAME = "remote_sources";
101   private static final Logger LOG = Logger.getInstance("#" + PythonSdkType.class.getName());
102   private static final String[] WINDOWS_EXECUTABLE_SUFFIXES = new String[]{"cmd", "exe", "bat", "com"};
103
104   static final int MINUTE = 60 * 1000; // 60 seconds, used with script timeouts
105   @NonNls public static final String SKELETONS_TOPIC = "Skeletons";
106   private static final String[] DIRS_WITH_BINARY = new String[]{"", "bin", "Scripts"};
107   private static final String[] UNIX_BINARY_NAMES = new String[]{"jython", "pypy", "python"};
108   private static final String[] WIN_BINARY_NAMES = new String[]{"jython.bat", "ipy.exe", "pypy.exe", "python.exe"};
109
110   private static final Key<WeakReference<Component>> SDK_CREATOR_COMPONENT_KEY = Key.create("#com.jetbrains.python.sdk.creatorComponent");
111
112   public static PythonSdkType getInstance() {
113     return SdkType.findInstance(PythonSdkType.class);
114   }
115
116   public PythonSdkType() {
117     super("Python SDK");
118   }
119
120   protected PythonSdkType(@NonNls String name) {
121     super(name);
122   }
123
124   public Icon getIcon() {
125     return PythonIcons.Python.Python;
126   }
127
128   @NotNull
129   @Override
130   public String getHelpTopic() {
131     return "reference.project.structure.sdk.python";
132   }
133
134   public Icon getIconForAddAction() {
135     return PythonFileType.INSTANCE.getIcon();
136   }
137
138   /**
139    * Name of directory where skeleton files (despite the value) are stored.
140    */
141   public static final String SKELETON_DIR_NAME = "python_stubs";
142
143   /**
144    * @return name of builtins skeleton file; for Python 2.x it is '{@code __builtins__.py}'.
145    */
146   @NotNull
147   @NonNls
148   public static String getBuiltinsFileName(@NotNull Sdk sdk) {
149     final LanguageLevel level = getLanguageLevelForSdk(sdk);
150     return level.isOlderThan(LanguageLevel.PYTHON30) ? PyBuiltinCache.BUILTIN_FILE : PyBuiltinCache.BUILTIN_FILE_3K;
151   }
152
153   @NonNls
154   @Nullable
155   public String suggestHomePath() {
156     final String pythonFromPath = findPythonInPath();
157     if (pythonFromPath != null) {
158       return pythonFromPath;
159     }
160     for (PythonSdkFlavor flavor : PythonSdkFlavor.getApplicableFlavors()) {
161       TreeSet<String> candidates = createVersionSet();
162       candidates.addAll(flavor.suggestHomePaths());
163       if (!candidates.isEmpty()) {
164         // return latest version
165         String[] candidateArray = ArrayUtil.toStringArray(candidates);
166         return candidateArray[candidateArray.length - 1];
167       }
168     }
169     return null;
170   }
171
172   @Nullable
173   private static String findPythonInPath() {
174     final String defaultCommand = SystemInfo.isWindows ? "python.exe" : "python";
175     final String path = System.getenv("PATH");
176     for (String root : path.split(File.pathSeparator)) {
177       final File file = new File(root, defaultCommand);
178       if (file.exists()) {
179         try {
180           return file.getCanonicalPath();
181         }
182         catch (IOException ignored) {
183         }
184       }
185     }
186     return null;
187   }
188
189   @NotNull
190   @Override
191   public Collection<String> suggestHomePaths() {
192     List<String> candidates = new ArrayList<String>();
193     for (PythonSdkFlavor flavor : PythonSdkFlavor.getApplicableFlavors()) {
194       candidates.addAll(flavor.suggestHomePaths());
195     }
196     return candidates;
197   }
198
199   private static TreeSet<String> createVersionSet() {
200     return new TreeSet<String>(new Comparator<String>() {
201       public int compare(String o1, String o2) {
202         return findDigits(o1).compareTo(findDigits(o2));
203       }
204     });
205   }
206
207   private static String findDigits(String s) {
208     int pos = StringUtil.findFirst(s, new CharFilter() {
209       public boolean accept(char ch) {
210         return Character.isDigit(ch);
211       }
212     });
213     if (pos >= 0) {
214       return s.substring(pos);
215     }
216     return s;
217   }
218
219   public static boolean hasValidSdk() {
220     for (Sdk sdk : ProjectJdkTable.getInstance().getAllJdks()) {
221       if (sdk.getSdkType() instanceof PythonSdkType) {
222         return true;
223       }
224     }
225     return false;
226   }
227
228   public boolean isValidSdkHome(@Nullable final String path) {
229     return PythonSdkFlavor.getFlavor(path) != null;
230   }
231
232   public static boolean isInvalid(@NotNull Sdk sdk) {
233     if (isRemote(sdk)) {
234       return false;
235     }
236     final VirtualFile interpreter = sdk.getHomeDirectory();
237     return interpreter == null || !interpreter.exists();
238   }
239
240   public static boolean isRemote(@Nullable Sdk sdk) {
241     return PySdkUtil.isRemote(sdk);
242   }
243
244   public static boolean isVagrant(@Nullable Sdk sdk) {
245     if (sdk != null && sdk.getSdkAdditionalData() instanceof PyRemoteSdkAdditionalDataBase) {
246       PyRemoteSdkAdditionalDataBase data = (PyRemoteSdkAdditionalDataBase)sdk.getSdkAdditionalData();
247
248       return data.getRemoteConnectionType() == CredentialsType.VAGRANT;
249     }
250     return false;
251   }
252
253   public static boolean isRemote(@Nullable String sdkPath) {
254     return isRemote(findSdkByPath(sdkPath));
255   }
256
257   @Override
258   public FileChooserDescriptor getHomeChooserDescriptor() {
259     final boolean isWindows = SystemInfo.isWindows;
260     return new FileChooserDescriptor(true, false, false, false, false, false) {
261       @Override
262       public void validateSelectedFiles(VirtualFile[] files) throws Exception {
263         if (files.length != 0) {
264           if (!isValidSdkHome(files[0].getPath())) {
265             throw new Exception(PyBundle.message("sdk.error.invalid.interpreter.name.$0", files[0].getName()));
266           }
267         }
268       }
269
270       @Override
271       public boolean isFileVisible(VirtualFile file, boolean showHiddenFiles) {
272         // TODO: add a better, customizable filtering
273         if (!file.isDirectory()) {
274           if (isWindows) {
275             String path = file.getPath();
276             boolean looksExecutable = false;
277             for (String ext : WINDOWS_EXECUTABLE_SUFFIXES) {
278               if (path.endsWith(ext)) {
279                 looksExecutable = true;
280                 break;
281               }
282             }
283             return looksExecutable && super.isFileVisible(file, showHiddenFiles);
284           }
285         }
286         return super.isFileVisible(file, showHiddenFiles);
287       }
288     }.withTitle(PyBundle.message("sdk.select.path")).withShowHiddenFiles(SystemInfo.isUnix);
289   }
290
291   public boolean supportsCustomCreateUI() {
292     return true;
293   }
294
295   public void showCustomCreateUI(SdkModel sdkModel, final JComponent parentComponent, final Consumer<Sdk> sdkCreatedCallback) {
296     Project project = CommonDataKeys.PROJECT.getData(DataManager.getInstance().getDataContext(parentComponent));
297     final PointerInfo pointerInfo = MouseInfo.getPointerInfo();
298     if (pointerInfo == null) return;
299     final Point point = pointerInfo.getLocation();
300     PythonSdkDetailsStep
301       .show(project, sdkModel.getSdks(), null, parentComponent, point, new NullableConsumer<Sdk>() {
302         @Override
303         public void consume(@Nullable Sdk sdk) {
304           if (sdk != null) {
305             sdk.putUserData(SDK_CREATOR_COMPONENT_KEY, new WeakReference<Component>(parentComponent));
306             sdkCreatedCallback.consume(sdk);
307           }
308         }
309       });
310   }
311
312   public static boolean isVirtualEnv(Sdk sdk) {
313     final String path = sdk.getHomePath();
314     return path != null && getVirtualEnvRoot(path) != null;
315   }
316
317   public static boolean isCondaVirtualEnv(Sdk sdk) {
318     final String path = sdk.getHomePath();
319     return path != null && PyCondaPackageManagerImpl.isCondaVEnv(sdk);
320   }
321
322   @Nullable
323   public Sdk getVirtualEnvBaseSdk(Sdk sdk) {
324     if (isVirtualEnv(sdk)) {
325       final PythonSdkFlavor flavor = PythonSdkFlavor.getFlavor(sdk);
326       final String version = getVersionString(sdk);
327       if (flavor != null && version != null) {
328         for (Sdk baseSdk : getAllSdks()) {
329           final PythonSdkFlavor baseFlavor = PythonSdkFlavor.getFlavor(baseSdk);
330           if (!isVirtualEnv(baseSdk) && flavor.equals(baseFlavor) && version.equals(getVersionString(baseSdk))) {
331             return baseSdk;
332           }
333         }
334       }
335     }
336     return null;
337   }
338
339   /**
340    * @param binaryPath must point to a Python interpreter
341    * @return if the surroundings look like a virtualenv installation, its root is returned (normally the grandparent of binaryPath).
342    */
343   @Nullable
344   public static File getVirtualEnvRoot(@NotNull final String binaryPath) {
345     final File bin = new File(binaryPath).getParentFile();
346     if (bin != null) {
347       final String rootPath = bin.getParent();
348       if (rootPath != null) {
349         final File root = new File(rootPath);
350         final File activateThis = new File(bin, "activate_this.py");
351         // binaryPath should contain an 'activate' script, and root should have bin (with us) and include and libp
352         if (activateThis.exists()) {
353           final File activate = findExecutableFile(bin, "activate");
354           if (activate != null) {
355             return root;
356           }
357         }
358         // Python 3.3 virtualenvs can be found as described in PEP 405
359         final String pyVenvCfg = "pyvenv.cfg";
360         if (new File(root, pyVenvCfg).exists() || new File(bin, pyVenvCfg).exists()) {
361           return root;
362         }
363       }
364     }
365     return null;
366   }
367
368   /**
369    * Finds a file that looks executable: an .exe or .cmd under windows, plain file under *nix.
370    *
371    * @param parent directory to look at
372    * @param name   name of the executable without suffix
373    * @return File representing the executable, or null.
374    */
375   @Nullable
376   public static File findExecutableFile(File parent, String name) {
377     if (SystemInfo.isWindows || SystemInfo.isOS2) {
378       for (String suffix : WINDOWS_EXECUTABLE_SUFFIXES) {
379         File file = new File(parent, name + "." + suffix);
380         if (file.exists()) return file;
381       }
382     }
383     else if (SystemInfo.isUnix) {
384       File file = new File(parent, name);
385       if (file.exists()) return file;
386     }
387     return null;
388   }
389
390   /**
391    * Alters PATH so that a virtualenv is activated, if present.
392    *
393    * @param commandLine           what to patch
394    * @param sdkHome               home of SDK we're using
395    * @param passParentEnvironment iff true, include system paths in PATH
396    */
397   public static void patchCommandLineForVirtualenv(GeneralCommandLine commandLine, String sdkHome, boolean passParentEnvironment) {
398     File virtualEnvRoot = getVirtualEnvRoot(sdkHome);
399     if (virtualEnvRoot != null) {
400       @NonNls final String PATH = "PATH";
401
402       // prepend virtualenv bin if it's not already on PATH
403       File bin = new File(virtualEnvRoot, "bin");
404       if (!bin.exists()) {
405         bin = new File(virtualEnvRoot, "Scripts");   // on Windows
406       }
407       String virtualenvBin = bin.getPath();
408
409       Map<String, String> env = commandLine.getEnvironment();
410       String pathValue;
411       if (env.containsKey(PATH)) {
412         pathValue = PythonEnvUtil.appendToPathEnvVar(env.get(PATH), virtualenvBin);
413       }
414       else if (passParentEnvironment) {
415         // append to PATH
416         pathValue = PythonEnvUtil.appendToPathEnvVar(System.getenv(PATH), virtualenvBin);
417       }
418       else {
419         pathValue = virtualenvBin;
420       }
421       env.put(PATH, pathValue);
422     }
423   }
424
425   public String suggestSdkName(final String currentSdkName, final String sdkHome) {
426     String name = getVersionString(sdkHome);
427     return suggestSdkNameFromVersion(sdkHome, name);
428   }
429
430   public static String suggestSdkNameFromVersion(String sdkHome, String version) {
431     sdkHome = FileUtil.toSystemDependentName(sdkHome);
432     final String shortHomeName = FileUtil.getLocationRelativeToUserHome(sdkHome);
433     if (version != null) {
434       File virtualEnvRoot = getVirtualEnvRoot(sdkHome);
435       if (virtualEnvRoot != null) {
436         version += " virtualenv at " + FileUtil.getLocationRelativeToUserHome(virtualEnvRoot.getAbsolutePath());
437       }
438       else {
439         version += " (" + shortHomeName + ")";
440       }
441     }
442     else {
443       version = "Unknown at " + shortHomeName;
444     } // last resort
445     return version;
446   }
447
448   @Nullable
449   public AdditionalDataConfigurable createAdditionalDataConfigurable(final SdkModel sdkModel, final SdkModificator sdkModificator) {
450     return null;
451   }
452
453   @Override
454   public void saveAdditionalData(@NotNull final SdkAdditionalData additionalData, @NotNull final Element additional) {
455     if (additionalData instanceof PythonSdkAdditionalData) {
456       ((PythonSdkAdditionalData)additionalData).save(additional);
457     }
458   }
459
460   @Override
461   public SdkAdditionalData loadAdditionalData(@NotNull final Sdk currentSdk, final Element additional) {
462     if (RemoteSdkCredentialsHolder.isRemoteSdk(currentSdk.getHomePath())) {
463       PythonRemoteInterpreterManager manager = PythonRemoteInterpreterManager.getInstance();
464       if (manager != null) {
465         return manager.loadRemoteSdkData(currentSdk, additional);
466       }
467     }
468     return PythonSdkAdditionalData.load(currentSdk, additional);
469   }
470
471   public static boolean isSkeletonsPath(String path) {
472     return path.contains(SKELETON_DIR_NAME);
473   }
474
475   @NonNls
476   public String getPresentableName() {
477     return "Python SDK";
478   }
479
480   @Override
481   public String sdkPath(VirtualFile homePath) {
482     String path = super.sdkPath(homePath);
483     PythonSdkFlavor flavor = PythonSdkFlavor.getFlavor(path);
484     if (flavor != null) {
485       VirtualFile sdkPath = flavor.getSdkPath(homePath);
486       if (sdkPath != null) {
487         return FileUtil.toSystemDependentName(sdkPath.getPath());
488       }
489     }
490     return FileUtil.toSystemDependentName(path);
491   }
492
493   public void setupSdkPaths(@NotNull final Sdk sdk) {
494     final Project project;
495     final WeakReference<Component> ownerComponentRef = sdk.getUserData(SDK_CREATOR_COMPONENT_KEY);
496     final Component ownerComponent = SoftReference.dereference(ownerComponentRef);
497     if (ownerComponent != null) {
498       project = CommonDataKeys.PROJECT.getData(DataManager.getInstance().getDataContext(ownerComponent));
499     }
500     else {
501       project = CommonDataKeys.PROJECT.getData(DataManager.getInstance().getDataContext());
502     }
503
504     DumbService.allowStartingDumbModeInside(DumbModePermission.MAY_START_BACKGROUND, new Runnable() {
505       @Override
506       public void run() {
507         setupSdkPaths(sdk, project, ownerComponent);
508       }
509     });
510   }
511
512   @Override
513   public boolean setupSdkPaths(Sdk sdk, SdkModel sdkModel) {
514     return true;  // run setupSdkPaths only once (from PythonSdkDetailsStep). Skip this from showCustomCreateUI
515   }
516
517   public static void setupSdkPaths(@NotNull final Sdk sdk,
518                                    @Nullable final Project project,
519                                    @Nullable final Component ownerComponent,
520                                    @NotNull final SdkModificator sdkModificator) {
521     doSetupSdkPaths(project, ownerComponent, PySdkUpdater.fromSdkModificator(sdk, sdkModificator));
522   }
523
524
525   public static void setupSdkPaths(final Sdk sdk, @Nullable final Project project, @Nullable final Component ownerComponent) {
526     ApplicationManager.getApplication().invokeLater(new Runnable() {
527       @Override
528       public void run() {
529         try {
530           final boolean success = doSetupSdkPaths(project, ownerComponent, PySdkUpdater.fromSdkPath(sdk.getHomePath()));
531
532           if (!success) {
533             Messages.showErrorDialog(
534               project,
535               PyBundle.message("MSG.cant.setup.sdk.$0", FileUtil.toSystemDependentName(sdk.getSdkModificator().getHomePath())),
536               PyBundle.message("MSG.title.bad.sdk")
537             );
538           }
539         }
540         catch (PySdkUpdater.PySdkNotFoundException e) {
541           // sdk was removed from sdk table so no need to setup paths
542         }
543       }
544     }, ModalityState.NON_MODAL);
545   }
546
547   private static boolean doSetupSdkPaths(@Nullable final Project project,
548                                          @Nullable final Component ownerComponent,
549                                          @NotNull final PySdkUpdater sdkUpdater) {
550     if (isRemote(sdkUpdater.getSdk()) && project == null && ownerComponent == null) {
551       LOG.error("For refreshing skeletons of remote SDK, either project or owner component must be specified");
552     }
553     final ProgressManager progressManager = ProgressManager.getInstance();
554     boolean sdkPathsUpdated = UIUtil.<Boolean>invokeAndWaitIfNeeded(
555       new Computable<Boolean>() {
556         @Override
557         public Boolean compute() {
558           return updateSdkPaths(sdkUpdater);
559         }
560       }
561     );
562
563     final Application application = ApplicationManager.getApplication();
564     if (sdkPathsUpdated && !application.isUnitTestMode()) {
565       application.invokeLater(new Runnable() {
566         @Override
567         public void run() {
568           progressManager.run(new Task.Backgroundable(project, PyBundle.message("sdk.gen.updating.skels"), false) {
569             @Override
570             public void run(@NotNull ProgressIndicator indicator) {
571               try {
572                 PythonSdkUpdater.updateSdk(project, ownerComponent, sdkUpdater);
573               }
574               catch (InvalidSdkException e) {
575                 // If the SDK is invalid, the user should worry about the SDK itself, not about skeletons generation errors
576                 if (isVagrant(sdkUpdater.getSdk())) {
577                   notifyRemoteSdkSkeletonsFail(e, new Runnable() {
578                     @Override
579                     public void run() {
580                       setupSdkPaths(sdkUpdater.getSdk(), project, ownerComponent);
581                     }
582                   });
583                 }
584                 else if (!isInvalid(sdkUpdater.getSdk())) {
585                   LOG.error(e);
586                 }
587               }
588             }
589           });
590         }
591       });
592     }
593     return sdkPathsUpdated;
594   }
595
596   @NotNull
597   public static Boolean updateSdkPaths(@NotNull PySdkUpdater sdkUpdater) {
598     sdkUpdater.modifySdk(new PySdkUpdater.SdkModificationProcessor() {
599       @Override
600       public void process(@NotNull Sdk sdk,
601                           @NotNull SdkModificator sdkModificator) {
602         sdkModificator.removeAllRoots();
603       }
604     });
605     try {
606       updateSdkRootsFromSysPath(sdkUpdater);
607       updateUserAddedPaths(sdkUpdater);
608       PythonSdkUpdater.getInstance()
609         .markAlreadyUpdated(sdkUpdater.getHomePath());
610       return true;
611     }
612     catch (InvalidSdkException ignored) {
613     }
614     return false;
615   }
616
617   public static void notifyRemoteSdkSkeletonsFail(final InvalidSdkException e, @Nullable final Runnable restartAction) {
618     NotificationListener notificationListener;
619
620     if (e.getCause() instanceof VagrantNotStartedException) {
621       notificationListener =
622         new NotificationListener() {
623           @Override
624           public void hyperlinkUpdate(@NotNull Notification notification, @NotNull HyperlinkEvent event) {
625             final PythonRemoteInterpreterManager manager = PythonRemoteInterpreterManager.getInstance();
626             if (manager != null) {
627               try {
628                 VagrantNotStartedException cause = (VagrantNotStartedException)e.getCause();
629                 manager.runVagrant(cause.getVagrantFolder(), cause.getMachineName());
630               }
631               catch (ExecutionException e1) {
632                 throw new RuntimeException(e1);
633               }
634             }
635             if (restartAction != null) {
636               restartAction.run();
637             }
638           }
639         };
640     }
641     else {
642       notificationListener = null;
643     }
644
645     Notifications.Bus.notify(
646       new Notification(
647         SKELETONS_TOPIC, "Couldn't refresh skeletons for remote interpreter",
648         e.getMessage() + "\n<a href=\"#\">Launch vagrant and refresh skeletons</a>",
649         NotificationType.WARNING,
650         notificationListener
651       )
652     );
653   }
654
655   /**
656    * In which root type built-in skeletons are put.
657    */
658   public static final OrderRootType BUILTIN_ROOT_TYPE = OrderRootType.CLASSES;
659
660   private final static Pattern PYTHON_NN_RE = Pattern.compile("python\\d\\.\\d.*");
661
662   public static void updateSdkRootsFromSysPath(PySdkUpdater sdkUpdater)
663     throws InvalidSdkException {
664     Application application = ApplicationManager.getApplication();
665     boolean not_in_unit_test_mode = (application != null && !application.isUnitTestMode());
666
667     String sdkHome = sdkUpdater.getHomePath();
668     assert sdkHome != null;
669     final String sep = File.separator;
670     // Add folders from sys.path
671     if (!PySdkUtil.isRemote(sdkUpdater.getSdk())) { //no sense to add roots of remote sdk
672       final List<String> paths = getSysPath(sdkHome);
673       if (paths.size() > 0) {
674         // add every path as root.
675         for (String path : paths) {
676           if (!path.contains(sep)) continue; // TODO: interpret possible 'special' paths reasonably
677           addSdkRoot(sdkUpdater, path);
678         }
679       }
680     }
681
682     PyUserSkeletonsUtil.addUserSkeletonsRoot(sdkUpdater);
683     addSkeletonsRoot(sdkUpdater, sdkHome);
684
685     if (not_in_unit_test_mode) {
686       File venv_root = getVirtualEnvRoot(sdkHome);
687       if (venv_root != null && venv_root.isDirectory()) {
688         File lib_root = new File(venv_root, "lib");
689         if (lib_root.isDirectory()) {
690           String[] inside = lib_root.list();
691           for (String s : inside) {
692             if (PYTHON_NN_RE.matcher(s).matches()) {
693               File py_lib_root = new File(lib_root, s);
694               if (new File(py_lib_root, "no-global-site-packages.txt").exists()) return; // don't add hardcoded paths
695             }
696           }
697         }
698       }
699       addHardcodedPaths(sdkUpdater);
700     }
701   }
702
703   public static void updateUserAddedPaths(PySdkUpdater sdkUpdater)
704     throws InvalidSdkException {
705     SdkAdditionalData data = sdkUpdater.getSdk().getSdkAdditionalData();
706     if (data instanceof PythonSdkAdditionalData) {
707       for (VirtualFile file : ((PythonSdkAdditionalData)data).getAddedPathFiles()) {
708         addSdkRoot(sdkUpdater, file);
709       }
710     }
711   }
712
713   private static void addSkeletonsRoot(@NotNull PySdkUpdater sdkUpdater, String sdkHome) {
714     @NonNls final String skeletonsPath = getSkeletonsPath(PathManager.getSystemPath(), sdkHome);
715     new File(skeletonsPath).mkdirs();
716     final VirtualFile builtins_root = LocalFileSystem.getInstance().refreshAndFindFileByPath(skeletonsPath);
717     assert builtins_root != null : "Cannot find skeletons path " + skeletonsPath + " in VFS";
718     sdkUpdater.addRoot(builtins_root, BUILTIN_ROOT_TYPE);
719   }
720
721   protected static void addHardcodedPaths(PySdkUpdater sdkUpdater) {
722     // Add python-django installed as package in Linux
723     // NOTE: fragile and arbitrary
724     if (SystemInfo.isLinux) {
725       final VirtualFile file = LocalFileSystem.getInstance().findFileByPath("/usr/lib/python-django");
726       if (file != null) {
727         sdkUpdater.addRoot(file, OrderRootType.CLASSES);
728       }
729     }
730   }
731
732   public static void addSdkRoot(PySdkUpdater sdkUpdater, String path) {
733     final VirtualFile file = LocalFileSystem.getInstance().refreshAndFindFileByPath(path);
734     if (file != null) {
735       addSdkRoot(sdkUpdater, file);
736     }
737     else {
738       LOG.info("Bogus sys.path entry " + path);
739     }
740   }
741
742   private static void addSdkRoot(@NotNull PySdkUpdater sdkUpdater, final @NotNull VirtualFile child) {
743     // NOTE: Files marked as library sources are not considered part of project source. Since the directory of the project the
744     // user is working on is included in PYTHONPATH with many configurations (e.g. virtualenv), we must not mark SDK paths as
745     // library sources, only as classes.
746     sdkUpdater.addRoot(getSdkRootVirtualFile(child), OrderRootType.CLASSES);
747   }
748
749   @NotNull
750   public static VirtualFile getSdkRootVirtualFile(@NotNull VirtualFile path) {
751     String suffix = path.getExtension();
752     if (suffix != null) {
753       suffix = suffix.toLowerCase(); // Why on earth empty suffix is null and not ""?
754     }
755     if ((!path.isDirectory()) && ("zip".equals(suffix) || "egg".equals(suffix))) {
756       // a .zip / .egg file must have its root extracted first
757       final VirtualFile jar = JarFileSystem.getInstance().getJarRootForLocalFile(path);
758       if (jar != null) {
759         return jar;
760       }
761     }
762     return path;
763   }
764
765   /**
766    * Returns skeletons location on the local machine. Independent of SDK credentials type (e.g. ssh, Vagrant, Docker or else).
767    */
768   public static String getSkeletonsPath(String basePath, String sdkHome) {
769     String sep = File.separator;
770     return getSkeletonsRootPath(basePath) + sep + FileUtil.toSystemIndependentName(sdkHome).hashCode() + sep;
771   }
772
773   public static String getSkeletonsRootPath(String basePath) {
774     return basePath + File.separator + SKELETON_DIR_NAME;
775   }
776
777   @NotNull
778   public static List<String> getSysPath(String bin_path) throws InvalidSdkException {
779     String working_dir = new File(bin_path).getParent();
780     Application application = ApplicationManager.getApplication();
781     if (application != null && !application.isUnitTestMode()) {
782       return getSysPathsFromScript(bin_path);
783     }
784     else { // mock sdk
785       List<String> ret = new ArrayList<String>(1);
786       ret.add(working_dir);
787       return ret;
788     }
789   }
790
791   @NotNull
792   public static List<String> getSysPathsFromScript(@NotNull String binaryPath) throws InvalidSdkException {
793     // to handle the situation when PYTHONPATH contains ., we need to run the syspath script in the
794     // directory of the script itself - otherwise the dir in which we run the script (e.g. /usr/bin) will be added to SDK path
795     GeneralCommandLine cmd = PythonHelper.SYSPATH.newCommandLine(binaryPath, Lists.<String>newArrayList());
796     final ProcessOutput runResult = PySdkUtil.getProcessOutput(cmd, new File(binaryPath).getParent(),
797                                                                 getVirtualEnvExtraEnv(binaryPath), MINUTE);
798     if (!runResult.checkSuccess(LOG)) {
799       throw new InvalidSdkException(String.format("Failed to determine Python's sys.path value:\nSTDOUT: %s\nSTDERR: %s",
800                                                   runResult.getStdout(),
801                                                   runResult.getStderr()));
802     }
803     return runResult.getStdoutLines();
804   }
805
806   /**
807    * Returns a piece of env good as additional env for getProcessOutput.
808    */
809   @Nullable
810   public static Map<String, String> getVirtualEnvExtraEnv(@NotNull String binaryPath) {
811     final File root = getVirtualEnvRoot(binaryPath);
812     if (root != null) {
813       return ImmutableMap.of("PATH", root.toString());
814     }
815     return null;
816   }
817
818   @Nullable
819   @Override
820   public String getVersionString(@NotNull Sdk sdk) {
821     if (isRemote(sdk)) {
822       final PyRemoteSdkAdditionalDataBase data = (PyRemoteSdkAdditionalDataBase)sdk.getSdkAdditionalData();
823       assert data != null;
824       String versionString = data.getVersionString();
825       if (StringUtil.isEmpty(versionString)) {
826         final PythonRemoteInterpreterManager remoteInterpreterManager = PythonRemoteInterpreterManager.getInstance();
827         if (remoteInterpreterManager != null) {
828           try {
829             versionString =
830               remoteInterpreterManager.getInterpreterVersion(null, data);
831           }
832           catch (Exception e) {
833             LOG.warn("Couldn't get interpreter version:" + e.getMessage(), e);
834             versionString = "undefined";
835           }
836         }
837         data.setVersionString(versionString);
838       }
839       return versionString;
840     }
841     else {
842       return getVersionString(sdk.getHomePath());
843     }
844   }
845
846   @Nullable
847   public String getVersionString(final String sdkHome) {
848     final PythonSdkFlavor flavor = PythonSdkFlavor.getFlavor(sdkHome);
849     return flavor != null ? flavor.getVersionString(sdkHome) : null;
850   }
851
852   public static List<Sdk> getAllSdks() {
853     return ProjectJdkTable.getInstance().getSdksOfType(getInstance());
854   }
855
856   @Nullable
857   public static Sdk findPythonSdk(@Nullable Module module) {
858     if (module == null) return null;
859     final Sdk sdk = ModuleRootManager.getInstance(module).getSdk();
860     if (sdk != null && sdk.getSdkType() instanceof PythonSdkType) return sdk;
861     final Facet[] facets = FacetManager.getInstance(module).getAllFacets();
862     for (Facet facet : facets) {
863       final FacetConfiguration configuration = facet.getConfiguration();
864       if (configuration instanceof PythonFacetSettings) {
865         return ((PythonFacetSettings)configuration).getSdk();
866       }
867     }
868     return null;
869   }
870
871   @Nullable
872   public static Sdk findSdkByPath(@Nullable String path) {
873     if (path != null) {
874       return findSdkByPath(getAllSdks(), path);
875     }
876     return null;
877   }
878
879   @Nullable
880   public static Sdk findSdkByPath(List<Sdk> sdkList, @Nullable String path) {
881     if (path != null) {
882       for (Sdk sdk : sdkList) {
883         if (sdk != null && FileUtil.pathsEqual(path, sdk.getHomePath())) {
884           return sdk;
885         }
886       }
887     }
888     return null;
889   }
890
891   @NotNull
892   public static LanguageLevel getLanguageLevelForSdk(@Nullable Sdk sdk) {
893     if (sdk != null && sdk.getSdkType() instanceof PythonSdkType) {
894       final PythonSdkFlavor flavor = PythonSdkFlavor.getFlavor(sdk);
895       if (flavor != null) {
896         return flavor.getLanguageLevel(sdk);
897       }
898     }
899     return LanguageLevel.getDefault();
900   }
901
902   public boolean isRootTypeApplicable(final OrderRootType type) {
903     return type == OrderRootType.CLASSES;
904   }
905
906   public boolean sdkHasValidPath(@NotNull Sdk sdk) {
907     if (PySdkUtil.isRemote(sdk)) {
908       return true;
909     }
910     VirtualFile homeDir = sdk.getHomeDirectory();
911     return homeDir != null && homeDir.isValid();
912   }
913
914   public static boolean isStdLib(VirtualFile vFile, Sdk pythonSdk) {
915     if (pythonSdk != null) {
916       final VirtualFile libDir = PyProjectScopeBuilder.findLibDir(pythonSdk);
917       if (libDir != null && VfsUtilCore.isAncestor(libDir, vFile, false)) {
918         return isNotSitePackages(vFile, libDir);
919       }
920       final VirtualFile venvLibDir = PyProjectScopeBuilder.findVirtualEnvLibDir(pythonSdk);
921       if (venvLibDir != null && VfsUtilCore.isAncestor(venvLibDir, vFile, false)) {
922         return isNotSitePackages(vFile, venvLibDir);
923       }
924       final VirtualFile skeletonsDir = PySdkUtil.findSkeletonsDir(pythonSdk);
925       if (skeletonsDir != null &&
926           Comparing.equal(vFile.getParent(), skeletonsDir)) {   // note: this will pick up some of the binary libraries not in packages
927         return true;
928       }
929     }
930     return false;
931   }
932
933   private static boolean isNotSitePackages(VirtualFile vFile, VirtualFile libDir) {
934     final VirtualFile sitePackages = libDir.findChild(PyNames.SITE_PACKAGES);
935     if (sitePackages != null && VfsUtilCore.isAncestor(sitePackages, vFile, false)) {
936       return false;
937     }
938     return true;
939   }
940
941   @Nullable
942   public static Sdk findPython2Sdk(@Nullable Module module) {
943     Sdk moduleSDK = findPythonSdk(module);
944     if (moduleSDK != null && !getLanguageLevelForSdk(moduleSDK).isPy3K()) {
945       return moduleSDK;
946     }
947     List<Sdk> allSdks = getAllSdks();
948     Collections.sort(allSdks, PreferredSdkComparator.INSTANCE);
949     for (Sdk sdk : allSdks) {
950       if (!getLanguageLevelForSdk(sdk).isPy3K()) {
951         return sdk;
952       }
953     }
954     return null;
955   }
956
957   @Nullable
958   public static Sdk findPython2Sdk(List<Sdk> sdks) {
959     Collections.sort(sdks, PreferredSdkComparator.INSTANCE);
960     for (Sdk sdk : sdks) {
961       if (!getLanguageLevelForSdk(sdk).isPy3K()) {
962         return sdk;
963       }
964     }
965     return null;
966   }
967
968   @Nullable
969   public static Sdk findLocalCPython(@Nullable Module module) {
970     Sdk moduleSDK = findPythonSdk(module);
971     if (moduleSDK != null && !isRemote(moduleSDK) && PythonSdkFlavor.getFlavor(moduleSDK) instanceof CPythonSdkFlavor) {
972       return moduleSDK;
973     }
974     List<Sdk> allSdks = getAllSdks();
975     Collections.sort(allSdks, PreferredSdkComparator.INSTANCE);
976     for (Sdk sdk : allSdks) {
977       if (!isRemote(sdk)) {
978         return sdk;
979       }
980     }
981     return null;
982   }
983
984   @Nullable
985   public static String getPythonExecutable(@NotNull String rootPath) {
986     final File rootFile = new File(rootPath);
987     if (rootFile.isFile()) {
988       return rootFile.getAbsolutePath();
989     }
990     for (String dir : DIRS_WITH_BINARY) {
991       final File subDir;
992       if (StringUtil.isEmpty(dir)) {
993         subDir = rootFile;
994       }
995       else {
996         subDir = new File(rootFile, dir);
997       }
998       if (!subDir.isDirectory()) {
999         continue;
1000       }
1001       for (String binaryName : getBinaryNames()) {
1002         final File executable = new File(subDir, binaryName);
1003         if (executable.isFile()) {
1004           return executable.getAbsolutePath();
1005         }
1006       }
1007     }
1008     return null;
1009   }
1010
1011   @Nullable
1012   public static String getExecutablePath(@NotNull final String homeDirectory, @NotNull String name) {
1013     File binPath = new File(homeDirectory);
1014     File binDir = binPath.getParentFile();
1015     if (binDir == null) return null;
1016     File runner = new File(binDir, name);
1017     if (runner.exists()) return LocalFileSystem.getInstance().extractPresentableUrl(runner.getPath());
1018     runner = new File(new File(binDir, "Scripts"), name);
1019     if (runner.exists()) return LocalFileSystem.getInstance().extractPresentableUrl(runner.getPath());
1020     runner = new File(new File(binDir.getParentFile(), "Scripts"), name);
1021     if (runner.exists()) return LocalFileSystem.getInstance().extractPresentableUrl(runner.getPath());
1022     runner = new File(new File(binDir.getParentFile(), "local"), name);
1023     if (runner.exists()) return LocalFileSystem.getInstance().extractPresentableUrl(runner.getPath());
1024     runner = new File(new File(new File(binDir.getParentFile(), "local"), "bin"), name);
1025     if (runner.exists()) return LocalFileSystem.getInstance().extractPresentableUrl(runner.getPath());
1026
1027     // if interpreter is a symlink
1028     if (FileSystemUtil.isSymLink(homeDirectory)) {
1029       String resolvedPath = FileSystemUtil.resolveSymLink(homeDirectory);
1030       if (resolvedPath != null) {
1031         return getExecutablePath(resolvedPath, name);
1032       }
1033     }
1034     // Search in standard unix path
1035     runner = new File(new File("/usr", "bin"), name);
1036     if (runner.exists()) return LocalFileSystem.getInstance().extractPresentableUrl(runner.getPath());
1037     runner = new File(new File(new File("/usr", "local"), "bin"), name);
1038     if (runner.exists()) return LocalFileSystem.getInstance().extractPresentableUrl(runner.getPath());
1039     return null;
1040   }
1041
1042   private static String[] getBinaryNames() {
1043     if (SystemInfo.isUnix) {
1044       return UNIX_BINARY_NAMES;
1045     }
1046     else {
1047       return WIN_BINARY_NAMES;
1048     }
1049   }
1050
1051   public static boolean isIncompleteRemote(Sdk sdk) {
1052     if (PySdkUtil.isRemote(sdk)) {
1053       //noinspection ConstantConditions
1054       if (!((PyRemoteSdkAdditionalDataBase)sdk.getSdkAdditionalData()).isValid()) {
1055         return true;
1056       }
1057     }
1058     return false;
1059   }
1060
1061   public static boolean hasInvalidRemoteCredentials(Sdk sdk) {
1062     if (PySdkUtil.isRemote(sdk)) {
1063       final Ref<Boolean> result = Ref.create(false);
1064       //noinspection ConstantConditions
1065       ((PyRemoteSdkAdditionalDataBase)sdk.getSdkAdditionalData()).switchOnConnectionType(new RemoteSdkConnectionAcceptor() {
1066         @Override
1067         public void ssh(@NotNull RemoteCredentialsHolder cred) {
1068         }
1069
1070         @Override
1071         public void vagrant(@NotNull VagrantBasedCredentialsHolder cred) {
1072           result.set(StringUtil.isEmpty(cred.getVagrantFolder()));
1073         }
1074
1075         @Override
1076         public void deployment(@NotNull WebDeploymentCredentialsHolder cred) {
1077         }
1078
1079         @Override
1080         public void docker(@NotNull DockerCredentialsHolder credentials) {
1081         }
1082       });
1083       return result.get();
1084     }
1085     return false;
1086   }
1087
1088   @Nullable
1089   public static Sdk getSdk(@NotNull final PsiElement element) {
1090     Module module = ModuleUtilCore.findModuleForPsiElement(element);
1091     if (module == null) {
1092       return null;
1093     }
1094     return ModuleRootManager.getInstance(module).getSdk();
1095   }
1096 }
1097