c10bb098b3e5d6504a609393535797b75e26aa55
[idea/community.git] / python / src / com / jetbrains / python / packaging / ui / PyPackageManagementService.java
1 /*
2  * Copyright 2000-2014 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.packaging.ui;
17
18 import com.google.common.collect.Lists;
19 import com.intellij.execution.ExecutionException;
20 import com.intellij.execution.RunCanceledByUserException;
21 import com.intellij.openapi.application.ApplicationManager;
22 import com.intellij.openapi.project.Project;
23 import com.intellij.openapi.projectRoots.Sdk;
24 import com.intellij.openapi.util.SystemInfo;
25 import com.intellij.openapi.util.text.StringUtil;
26 import com.intellij.util.CatchingConsumer;
27 import com.intellij.webcore.packaging.InstalledPackage;
28 import com.intellij.webcore.packaging.PackageManagementServiceEx;
29 import com.intellij.webcore.packaging.RepoPackage;
30 import com.jetbrains.python.packaging.*;
31 import com.jetbrains.python.packaging.PyPIPackageUtil.PackageDetails;
32 import com.jetbrains.python.psi.LanguageLevel;
33 import com.jetbrains.python.sdk.PySdkUtil;
34 import com.jetbrains.python.sdk.PythonSdkType;
35 import org.jetbrains.annotations.NonNls;
36 import org.jetbrains.annotations.NotNull;
37 import org.jetbrains.annotations.Nullable;
38
39 import java.io.IOException;
40 import java.util.*;
41 import java.util.regex.Matcher;
42 import java.util.regex.Pattern;
43
44 /**
45  * @author yole
46  */
47 public class PyPackageManagementService extends PackageManagementServiceEx {
48   @NotNull private static final Pattern PATTERN_ERROR_LINE = Pattern.compile(".*error:.*", Pattern.CASE_INSENSITIVE);
49   @NonNls private static final String TEXT_PREFIX = "<html><head>" +
50                                                     "    <style type=\"text/css\">" +
51                                                     "        p {" +
52                                                     "            font-family: Arial,serif; font-size: 12pt; margin: 2px 2px" +
53                                                     "        }" +
54                                                     "    </style>" +
55                                                     "</head><body style=\"font-family: Arial,serif; font-size: 12pt; margin: 5px 5px;\">";
56   @NonNls private static final String TEXT_SUFFIX = "</body></html>";
57
58   private final Project myProject;
59   protected final Sdk mySdk;
60
61   public PyPackageManagementService(@NotNull Project project, @NotNull Sdk sdk) {
62     myProject = project;
63     mySdk = sdk;
64   }
65
66   @NotNull
67   public Sdk getSdk() {
68     return mySdk;
69   }
70
71   @Nullable
72   @Override
73   public List<String> getAllRepositories() {
74     final PyPackageService packageService = PyPackageService.getInstance();
75     final List<String> result = new ArrayList<>();
76     if (!packageService.PYPI_REMOVED) result.add(PyPIPackageUtil.PYPI_LIST_URL);
77     result.addAll(packageService.additionalRepositories);
78     return result;
79   }
80
81   @Override
82   public void addRepository(String repositoryUrl) {
83     PyPackageService.getInstance().addRepository(repositoryUrl);
84   }
85
86   @Override
87   public void removeRepository(String repositoryUrl) {
88     PyPackageService.getInstance().removeRepository(repositoryUrl);
89   }
90
91   @NotNull
92   @Override
93   public List<RepoPackage> getAllPackages() throws IOException {
94     final Map<String, String> packageToVersionMap = PyPIPackageUtil.INSTANCE.loadAndGetPackages();
95     final List<RepoPackage> packages = versionMapToPackageList(packageToVersionMap);
96     packages.addAll(PyPIPackageUtil.INSTANCE.getAdditionalPackageNames());
97     return packages;
98   }
99
100   @NotNull
101   protected static List<RepoPackage> versionMapToPackageList(@NotNull Map<String, String> packageToVersionMap) {
102     final boolean customRepoConfigured = !PyPackageService.getInstance().additionalRepositories.isEmpty();
103     final String url = customRepoConfigured ? PyPIPackageUtil.PYPI_LIST_URL : "";
104     final List<RepoPackage> packages = new ArrayList<>();
105     for (Map.Entry<String, String> entry : packageToVersionMap.entrySet()) {
106       packages.add(new RepoPackage(entry.getKey(), url, entry.getValue()));
107     }
108     return packages;
109   }
110
111   @NotNull
112   @Override
113   public List<RepoPackage> reloadAllPackages() throws IOException {
114     PyPIPackageUtil.INSTANCE.clearPackagesCache();
115     return getAllPackages();
116   }
117
118   @NotNull
119   @Override
120   public List<RepoPackage> getAllPackagesCached() {
121     return versionMapToPackageList(PyPIPackageUtil.getPyPIPackages());
122   }
123
124   @Override
125   public boolean canInstallToUser() {
126     return !PythonSdkType.isVirtualEnv(mySdk);
127   }
128
129   @NotNull
130   @Override
131   public String getInstallToUserText() {
132     String userSiteText = "Install to user's site packages directory";
133     if (!PythonSdkType.isRemote(mySdk))
134       userSiteText += " (" + PySdkUtil.getUserSite() + ")";
135     return userSiteText;
136   }
137
138   @Override
139   public boolean isInstallToUserSelected() {
140     return PyPackageService.getInstance().useUserSite(mySdk.getHomePath());
141   }
142
143   @Override
144   public void installToUserChanged(boolean newValue) {
145     PyPackageService.getInstance().addSdkToUserSite(mySdk.getHomePath(), newValue);
146   }
147
148   @NotNull
149   @Override
150   public Collection<InstalledPackage> getInstalledPackages() throws IOException {
151
152     final PyPackageManager manager = PyPackageManager.getInstance(mySdk);
153     final List<PyPackage> packages;
154     try {
155       packages = Lists.newArrayList(manager.refreshAndGetPackages(true));
156     }
157     catch (ExecutionException e) {
158       throw new IOException(e);
159     }
160     Collections.sort(packages, (pkg1, pkg2) -> pkg1.getName().compareTo(pkg2.getName()));
161     return new ArrayList<>(packages);
162   }
163
164   @Override
165   public void installPackage(@NotNull RepoPackage repoPackage, @Nullable String version, boolean forceUpgrade, @Nullable String extraOptions,
166                              @NotNull Listener listener, boolean installToUser) {
167     final String packageName = repoPackage.getName();
168     final String repository = PyPIPackageUtil.isPyPIRepository(repoPackage.getRepoUrl()) ? null : repoPackage.getRepoUrl();
169     final List<String> extraArgs = new ArrayList<>();
170     if (installToUser) {
171       extraArgs.add(PyPackageManager.USE_USER_SITE);
172     }
173     if (extraOptions != null) {
174       // TODO: Respect arguments quotation
175       Collections.addAll(extraArgs, extraOptions.split(" +"));
176     }
177     if (!StringUtil.isEmptyOrSpaces(repository)) {
178       extraArgs.add("--index-url");
179       extraArgs.add(repository);
180     }
181     if (forceUpgrade) {
182       extraArgs.add("-U");
183     }
184     final PyRequirement req;
185     if (version != null) {
186       req = new PyRequirement(packageName, version);
187     }
188     else {
189       req = new PyRequirement(packageName);
190     }
191
192     final PyPackageManagerUI ui = new PyPackageManagerUI(myProject, mySdk, new PyPackageManagerUI.Listener() {
193       @Override
194       public void started() {
195         listener.operationStarted(packageName);
196       }
197
198       @Override
199       public void finished(@Nullable List<ExecutionException> exceptions) {
200         listener.operationFinished(packageName, toErrorDescription(exceptions, mySdk));
201       }
202     });
203     ui.install(Collections.singletonList(req), extraArgs);
204   }
205
206   @Nullable
207   public static ErrorDescription toErrorDescription(@Nullable List<ExecutionException> exceptions, @Nullable Sdk sdk) {
208     if (exceptions != null && !exceptions.isEmpty() && !isCancelled(exceptions)) {
209       return createDescription(exceptions.get(0), sdk);
210     }
211     return null;
212   }
213
214   @Override
215   public void uninstallPackages(@NotNull List<InstalledPackage> installedPackages, @NotNull Listener listener) {
216     final String packageName = installedPackages.size() == 1 ? installedPackages.get(0).getName() : null;
217     final PyPackageManagerUI ui = new PyPackageManagerUI(myProject, mySdk, new PyPackageManagerUI.Listener() {
218       @Override
219       public void started() {
220         listener.operationStarted(packageName);
221       }
222
223       @Override
224       public void finished(List<ExecutionException> exceptions) {
225         listener.operationFinished(packageName, toErrorDescription(exceptions, mySdk));
226       }
227     });
228
229     final List<PyPackage> pyPackages = new ArrayList<>();
230     for (InstalledPackage aPackage : installedPackages) {
231       if (aPackage instanceof PyPackage) {
232         pyPackages.add((PyPackage)aPackage);
233       }
234     }
235     ui.uninstall(pyPackages);
236   }
237
238   @Override
239   public void fetchPackageVersions(String packageName, CatchingConsumer<List<String>, Exception> consumer) {
240     PyPIPackageUtil.INSTANCE.usePackageReleases(packageName, new CatchingConsumer<List<String>, Exception>() {
241       @Override
242       public void consume(List<String> releases) {
243         if (releases != null) {
244           PyPIPackageUtil.INSTANCE.addPackageReleases(packageName, releases);
245           consumer.consume(releases);
246         }
247       }
248
249       @Override
250       public void consume(Exception e) {
251         consumer.consume(e);
252       }
253     });
254   }
255
256   @Override
257   public void fetchPackageDetails(@NotNull String packageName, @NotNull CatchingConsumer<String, Exception> consumer) {
258     PyPIPackageUtil.INSTANCE.fillPackageDetails(packageName, new CatchingConsumer<PackageDetails.Info, Exception>() {
259       @Override
260       public void consume(PackageDetails.Info details) {
261         consumer.consume(formatPackageInfo(details));
262       }
263
264       @Override
265       public void consume(Exception e) {
266         consumer.consume(e);
267       }
268     });
269   }
270
271   private static String formatPackageInfo(@NotNull PackageDetails.Info info) {
272     final StringBuilder stringBuilder = new StringBuilder(TEXT_PREFIX);
273     final String description = info.getSummary();
274     if (StringUtil.isNotEmpty(description)) {
275       stringBuilder.append(description).append("<br/>");
276     }
277     final String version = info.getVersion();
278     if (StringUtil.isNotEmpty(version)) {
279       stringBuilder.append("<h4>Version</h4>");
280       stringBuilder.append(version);
281     }
282     final String author = info.getAuthor();
283     if (StringUtil.isNotEmpty(author)) {
284       stringBuilder.append("<h4>Author</h4>");
285       stringBuilder.append(author).append("<br/><br/>");
286     }
287     final String authorEmail = info.getAuthorEmail();
288     if (StringUtil.isNotEmpty(authorEmail)) {
289       stringBuilder.append("<br/>");
290       stringBuilder.append(composeHref("mailto:" + authorEmail));
291     }
292     final String homePage = info.getHomePage();
293     if (StringUtil.isNotEmpty(homePage)) {
294       stringBuilder.append("<br/>");
295       stringBuilder.append(composeHref(homePage));
296     }
297     stringBuilder.append(TEXT_SUFFIX);
298     return stringBuilder.toString();
299   }
300
301   @NonNls private static final String HTML_PREFIX = "<a href=\"";
302   @NonNls private static final String HTML_SUFFIX = "</a>";
303
304   @NotNull
305   private static String composeHref(String vendorUrl) {
306     return HTML_PREFIX + vendorUrl + "\">" + vendorUrl + HTML_SUFFIX;
307   }
308
309   private static boolean isCancelled(@NotNull List<ExecutionException> exceptions) {
310     for (ExecutionException e : exceptions) {
311       if (e instanceof RunCanceledByUserException) {
312         return true;
313       }
314     }
315     return false;
316   }
317
318   @NotNull
319   private static ErrorDescription createDescription(@NotNull ExecutionException e, @Nullable Sdk sdk) {
320     if (e instanceof PyExecutionException) {
321       final PyExecutionException ee = (PyExecutionException)e;
322       final String stdoutCause = findErrorCause(ee.getStdout());
323       final String stderrCause = findErrorCause(ee.getStderr());
324       final String cause = stdoutCause != null ? stdoutCause : stderrCause;
325       final String message =  cause != null ? cause : ee.getMessage();
326       final String command = ee.getCommand() + " " + StringUtil.join(ee.getArgs(), " ");
327       return new ErrorDescription(message, command, ee.getStdout() + "\n" + ee.getStderr(), findErrorSolution(ee, cause, sdk));
328     }
329     else {
330       return ErrorDescription.fromMessage(e.getMessage());
331     }
332   }
333
334   @Nullable
335   private static String findErrorSolution(@NotNull PyExecutionException e, @Nullable String cause, @Nullable Sdk sdk) {
336     if (cause != null) {
337       if (StringUtil.containsIgnoreCase(cause, "SyntaxError")) {
338         final LanguageLevel languageLevel = PythonSdkType.getLanguageLevelForSdk(sdk);
339         return "Make sure that you use a version of Python supported by this package. Currently you are using Python " +
340                languageLevel + ".";
341       }
342     }
343
344     if (SystemInfo.isLinux && (containsInOutput(e, "pyconfig.h") || containsInOutput(e, "Python.h"))) {
345       return "Make sure that you have installed Python development packages for your operating system.";
346     }
347
348     if ("pip".equals(e.getCommand()) && sdk != null) {
349       return "Try to run this command from the system terminal. Make sure that you use the correct version of 'pip' " +
350              "installed for your Python interpreter located at '" + sdk.getHomePath() + "'.";
351     }
352
353     return null;
354   }
355
356   private static boolean containsInOutput(@NotNull PyExecutionException e, @NotNull String text) {
357     return StringUtil.containsIgnoreCase(e.getStdout(), text) || StringUtil.containsIgnoreCase(e.getStderr(), text);
358   }
359
360   @Nullable
361   private static String findErrorCause(@NotNull String output) {
362     final Matcher m = PATTERN_ERROR_LINE.matcher(output);
363     if (m.find()) {
364       final String result = m.group();
365       return result != null ? result.trim() : null;
366     }
367     return null;
368   }
369
370   @Override
371   public void updatePackage(@NotNull InstalledPackage installedPackage,
372                             @Nullable String version,
373                             @NotNull Listener listener) {
374     installPackage(new RepoPackage(installedPackage.getName(), null), null, true, null, listener, false);
375   }
376
377   @Override
378   public boolean shouldFetchLatestVersionsForOnlyInstalledPackages() {
379     /*
380     final List<String> repositories = PyPackageService.getInstance().additionalRepositories;
381     return repositories.size() > 1  || (repositories.size() == 1 && !repositories.get(0).equals(PyPIPackageUtil.PYPI_LIST_URL));
382     */
383     return true;
384   }
385
386   @Override
387   public void fetchLatestVersion(@NotNull InstalledPackage pkg, @NotNull CatchingConsumer<String, Exception> consumer) {
388     ApplicationManager.getApplication().executeOnPooledThread(() -> {
389       try {
390         PyPIPackageUtil.INSTANCE.loadAndGetPackages();
391         final String version = PyPIPackageUtil.INSTANCE.fetchLatestPackageVersion(pkg.getName());
392         consumer.consume(StringUtil.notNullize(version));
393       }
394       catch (IOException e) {
395         consumer.consume(e);
396       }
397     });
398   }
399 }