├── .github └── workflows │ └── test.yaml ├── .gitignore ├── .travis.yml ├── LICENSE ├── README.md ├── build.gradle ├── gradle └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat ├── scripts ├── publish.sh └── verify.sh ├── settings.gradle └── src ├── main └── groovy │ └── com │ └── jetbrains │ └── python │ └── envs │ ├── PythonEnvsExtension.groovy │ └── PythonEnvsPlugin.groovy └── test └── groovy └── com └── jetbrains └── python └── envs └── test └── FunctionalTest.groovy /.github/workflows/test.yaml: -------------------------------------------------------------------------------- 1 | name: test 2 | 3 | on: 4 | push: 5 | branches: 6 | - master 7 | 8 | jobs: 9 | build-plugin: 10 | runs-on: ubuntu-latest 11 | 12 | steps: 13 | - name: Checkout source code 14 | uses: actions/checkout@v4 15 | 16 | - name: Set up JDK 8 17 | uses: actions/setup-java@v4 18 | with: 19 | distribution: corretto 20 | java-version: 8 21 | 22 | - name: Setup Gradle 23 | uses: gradle/actions/setup-gradle@v3 24 | 25 | - name: Test 26 | run: ./gradlew test -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.iml 2 | /.gradle/ 3 | /.idea/ 4 | /build/ 5 | *.ipr 6 | *.iws 7 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: groovy 2 | 3 | os: 4 | - linux 5 | 6 | jdk: 7 | - openjdk8 8 | 9 | dist: xenial -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | 2 | Apache License 3 | Version 2.0, January 2004 4 | http://www.apache.org/licenses/ 5 | 6 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 7 | 8 | 1. Definitions. 9 | 10 | "License" shall mean the terms and conditions for use, reproduction, 11 | and distribution as defined by Sections 1 through 9 of this document. 12 | 13 | "Licensor" shall mean the copyright owner or entity authorized by 14 | the copyright owner that is granting the License. 15 | 16 | "Legal Entity" shall mean the union of the acting entity and all 17 | other entities that control, are controlled by, or are under common 18 | control with that entity. For the purposes of this definition, 19 | "control" means (i) the power, direct or indirect, to cause the 20 | direction or management of such entity, whether by contract or 21 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 22 | outstanding shares, or (iii) beneficial ownership of such entity. 23 | 24 | "You" (or "Your") shall mean an individual or Legal Entity 25 | exercising permissions granted by this License. 26 | 27 | "Source" form shall mean the preferred form for making modifications, 28 | including but not limited to software source code, documentation 29 | source, and configuration files. 30 | 31 | "Object" form shall mean any form resulting from mechanical 32 | transformation or translation of a Source form, including but 33 | not limited to compiled object code, generated documentation, 34 | and conversions to other media types. 35 | 36 | "Work" shall mean the work of authorship, whether in Source or 37 | Object form, made available under the License, as indicated by a 38 | copyright notice that is included in or attached to the work 39 | (an example is provided in the Appendix below). 40 | 41 | "Derivative Works" shall mean any work, whether in Source or Object 42 | form, that is based on (or derived from) the Work and for which the 43 | editorial revisions, annotations, elaborations, or other modifications 44 | represent, as a whole, an original work of authorship. For the purposes 45 | of this License, Derivative Works shall not include works that remain 46 | separable from, or merely link (or bind by name) to the interfaces of, 47 | the Work and Derivative Works thereof. 48 | 49 | "Contribution" shall mean any work of authorship, including 50 | the original version of the Work and any modifications or additions 51 | to that Work or Derivative Works thereof, that is intentionally 52 | submitted to Licensor for inclusion in the Work by the copyright owner 53 | or by an individual or Legal Entity authorized to submit on behalf of 54 | the copyright owner. For the purposes of this definition, "submitted" 55 | means any form of electronic, verbal, or written communication sent 56 | to the Licensor or its representatives, including but not limited to 57 | communication on electronic mailing lists, source code control systems, 58 | and issue tracking systems that are managed by, or on behalf of, the 59 | Licensor for the purpose of discussing and improving the Work, but 60 | excluding communication that is conspicuously marked or otherwise 61 | designated in writing by the copyright owner as "Not a Contribution." 62 | 63 | "Contributor" shall mean Licensor and any individual or Legal Entity 64 | on behalf of whom a Contribution has been received by Licensor and 65 | subsequently incorporated within the Work. 66 | 67 | 2. Grant of Copyright License. Subject to the terms and conditions of 68 | this License, each Contributor hereby grants to You a perpetual, 69 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 70 | copyright license to reproduce, prepare Derivative Works of, 71 | publicly display, publicly perform, sublicense, and distribute the 72 | Work and such Derivative Works in Source or Object form. 73 | 74 | 3. Grant of Patent License. Subject to the terms and conditions of 75 | this License, each Contributor hereby grants to You a perpetual, 76 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 77 | (except as stated in this section) patent license to make, have made, 78 | use, offer to sell, sell, import, and otherwise transfer the Work, 79 | where such license applies only to those patent claims licensable 80 | by such Contributor that are necessarily infringed by their 81 | Contribution(s) alone or by combination of their Contribution(s) 82 | with the Work to which such Contribution(s) was submitted. If You 83 | institute patent litigation against any entity (including a 84 | cross-claim or counterclaim in a lawsuit) alleging that the Work 85 | or a Contribution incorporated within the Work constitutes direct 86 | or contributory patent infringement, then any patent licenses 87 | granted to You under this License for that Work shall terminate 88 | as of the date such litigation is filed. 89 | 90 | 4. Redistribution. You may reproduce and distribute copies of the 91 | Work or Derivative Works thereof in any medium, with or without 92 | modifications, and in Source or Object form, provided that You 93 | meet the following conditions: 94 | 95 | (a) You must give any other recipients of the Work or 96 | Derivative Works a copy of this License; and 97 | 98 | (b) You must cause any modified files to carry prominent notices 99 | stating that You changed the files; and 100 | 101 | (c) You must retain, in the Source form of any Derivative Works 102 | that You distribute, all copyright, patent, trademark, and 103 | attribution notices from the Source form of the Work, 104 | excluding those notices that do not pertain to any part of 105 | the Derivative Works; and 106 | 107 | (d) If the Work includes a "NOTICE" text file as part of its 108 | distribution, then any Derivative Works that You distribute must 109 | include a readable copy of the attribution notices contained 110 | within such NOTICE file, excluding those notices that do not 111 | pertain to any part of the Derivative Works, in at least one 112 | of the following places: within a NOTICE text file distributed 113 | as part of the Derivative Works; within the Source form or 114 | documentation, if provided along with the Derivative Works; or, 115 | within a display generated by the Derivative Works, if and 116 | wherever such third-party notices normally appear. The contents 117 | of the NOTICE file are for informational purposes only and 118 | do not modify the License. You may add Your own attribution 119 | notices within Derivative Works that You distribute, alongside 120 | or as an addendum to the NOTICE text from the Work, provided 121 | that such additional attribution notices cannot be construed 122 | as modifying the License. 123 | 124 | You may add Your own copyright statement to Your modifications and 125 | may provide additional or different license terms and conditions 126 | for use, reproduction, or distribution of Your modifications, or 127 | for any such Derivative Works as a whole, provided Your use, 128 | reproduction, and distribution of the Work otherwise complies with 129 | the conditions stated in this License. 130 | 131 | 5. Submission of Contributions. Unless You explicitly state otherwise, 132 | any Contribution intentionally submitted for inclusion in the Work 133 | by You to the Licensor shall be under the terms and conditions of 134 | this License, without any additional terms or conditions. 135 | Notwithstanding the above, nothing herein shall supersede or modify 136 | the terms of any separate license agreement you may have executed 137 | with Licensor regarding such Contributions. 138 | 139 | 6. Trademarks. This License does not grant permission to use the trade 140 | names, trademarks, service marks, or product names of the Licensor, 141 | except as required for reasonable and customary use in describing the 142 | origin of the Work and reproducing the content of the NOTICE file. 143 | 144 | 7. Disclaimer of Warranty. Unless required by applicable law or 145 | agreed to in writing, Licensor provides the Work (and each 146 | Contributor provides its Contributions) on an "AS IS" BASIS, 147 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 148 | implied, including, without limitation, any warranties or conditions 149 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 150 | PARTICULAR PURPOSE. You are solely responsible for determining the 151 | appropriateness of using or redistributing the Work and assume any 152 | risks associated with Your exercise of permissions under this License. 153 | 154 | 8. Limitation of Liability. In no event and under no legal theory, 155 | whether in tort (including negligence), contract, or otherwise, 156 | unless required by applicable law (such as deliberate and grossly 157 | negligent acts) or agreed to in writing, shall any Contributor be 158 | liable to You for damages, including any direct, indirect, special, 159 | incidental, or consequential damages of any character arising as a 160 | result of this License or out of the use or inability to use the 161 | Work (including but not limited to damages for loss of goodwill, 162 | work stoppage, computer failure or malfunction, or any and all 163 | other commercial damages or losses), even if such Contributor 164 | has been advised of the possibility of such damages. 165 | 166 | 9. Accepting Warranty or Additional Liability. While redistributing 167 | the Work or Derivative Works thereof, You may choose to offer, 168 | and charge a fee for, acceptance of support, warranty, indemnity, 169 | or other liability obligations and/or rights consistent with this 170 | License. However, in accepting such obligations, You may act only 171 | on Your own behalf and on Your sole responsibility, not on behalf 172 | of any other Contributor, and only if You agree to indemnify, 173 | defend, and hold each Contributor harmless for any liability 174 | incurred by, or claims asserted against, such Contributor by reason 175 | of your accepting any such warranty or additional liability. 176 | 177 | END OF TERMS AND CONDITIONS 178 | 179 | APPENDIX: How to apply the Apache License to your work. 180 | 181 | To apply the Apache License to your work, attach the following 182 | boilerplate notice, with the fields enclosed by brackets "[]" 183 | replaced with your own identifying information. (Don't include 184 | the brackets!) The text should be enclosed in the appropriate 185 | comment syntax for the file format. We also recommend that a 186 | file or class name and description of purpose be included on the 187 | same "printed page" as the copyright notice for easier 188 | identification within third-party archives. 189 | 190 | Copyright 2014 Palantir Technologies 191 | 192 | Licensed under the Apache License, Version 2.0 (the "License"); 193 | you may not use this file except in compliance with the License. 194 | You may obtain a copy of the License at 195 | 196 | http://www.apache.org/licenses/LICENSE-2.0 197 | 198 | Unless required by applicable law or agreed to in writing, software 199 | distributed under the License is distributed on an "AS IS" BASIS, 200 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 201 | See the License for the specific language governing permissions and 202 | limitations under the License. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![official JetBrains project](https://jb.gg/badges/official-plastic.svg)](https://confluence.jetbrains.com/display/ALL/JetBrains+on+GitHub) 2 | 3 | Gradle Python Envs Plugin 4 | ======================== 5 | 6 | Gradle plugin to create Python envs. 7 | 8 | This plugin is based on [gradle-miniconda-plugin](https://github.com/palantir/gradle-miniconda-plugin), 9 | but in addition to creating Conda envs it provides: 10 | 11 | 1. A convenient DSL to specify target Python environments 12 | 2. Creating Python envs for Unix with [python-build](https://github.com/pyenv/pyenv/tree/master/plugins/python-build) for Unix 13 |
N.B.: [Common build problems](https://github.com/pyenv/pyenv/wiki/Common-build-problems) article from pyenv 14 | 3. Creating Python envs for Windows by installing msi or exe from [python.org](https://www.python.org/). 15 |
N.B.: Windows UAC should be switched off, otherwise - use Python from zip 16 | 4. Both Anaconda and Miniconda support (32 and 64 bit versions) 17 | 5. Creating Conda envrionments, conda package installation support 18 | 6. Creating Jython environments 19 | 7. Creating PyPy environments (only Unix is supported, by default pypy2.7-5.8.0 version is used) 20 | 8. Creating IronPython environments (only Windows is supported, by default [2.7.9 version](https://github.com/IronLanguages/ironpython2/releases/tag/ipy-2.7.9) is used) 21 | 9. Virtualenv creation from any python environment created 22 | 10. Python from zip creation: downloading archive from specified url, unpacking and preparing to work with 23 | 11. Package installation for any environment or virtualenv with specified install options 24 | 25 | 26 | Usage 27 | ----- 28 | 29 | Apply the plugin to your project following 30 | [`https://plugins.gradle.org/plugin/com.jetbrains.python.envs`](https://plugins.gradle.org/plugin/com.jetbrains.python.envs), 31 | and configure the associated extension: 32 | 33 | ```gradle 34 | envs { 35 | bootstrapDirectory = new File(buildDir, 'bootstrap') 36 | envsDirectory = new File(buildDir, 'envs') 37 | 38 | // Download python zips when Windows is used from https://repository.net/%archieveName%, 39 | // where {archieveName} is python-{version}-{architecture}.zip. 40 | // For example, for the 64 bit version of Python 3.7.2 the archive name will be python-3.7.2-64.zip 41 | zipRepository = new URL("https://repository.net/") 42 | shouldUseZipsFromRepository = Os.isFamily(Os.FAMILY_WINDOWS) 43 | 44 | // by default if architecture isn't specified - 64 bit one is used 45 | // _64Bits = true 46 | 47 | // by default pipInstallOptions equals to "--trusted-host pypi.python.org" 48 | // to fix CERTIFICATE_VERIFY_FAILED ssl error 49 | // pipInstallOptions = "--trusted-host pypi.python.org" 50 | 51 | //python "envName", "version", [] 52 | python "python35_64", "3.5.3", ["django==1.9"] 53 | //python "envName", "version", "architecture", [] 54 | python "python36_32", "3.6.2", "32", ["django==1.10"] 55 | //python "envName", "version", "architecture", [], patchUri 56 | python "python310_64", "3.10.0", "64", [], "file://path/to/a.patch" 57 | //virtualenv "virtualEnvName", "sourceEnvName", [] 58 | virtualenv "envPython35", "python35_64", ["pytest"] 59 | virtualenv "envPython36", "python36_32", ["behave", "requests"] 60 | 61 | //conda "envName", "version", "architecture" 62 | conda "Miniconda3", "Miniconda3-latest", "64" 63 | //conda "envName", "version", [] 64 | conda "Anaconda2", "Anaconda2-4.4.0", [condaPackage("PyQt")] 65 | //conda "envName", "version", "architecture", [] 66 | conda "Anaconda3", "Anaconda3-4.4.0", "64", ["django==1.8"] 67 | 68 | //condaenv "envName", "version", [] 69 | // Here will be created additional "Miniconda2-latest" 70 | // (or another one specified in condaDefaultVersion value) 71 | // conda interpreter to be bootstraped 72 | condaenv "pyqt_env", "2.7", [condaPackage("pyqt")] 73 | //condaenv "envName", "version", "sourceEnvName", [] 74 | condaenv "django19", "2.7", "Miniconda3", ["django==1.9"] 75 | condaenv "conda34", "3.4", "Miniconda3", ["ipython==2.1", "django==1.6", "behave", "jinja2", "tox==2.0"] 76 | 77 | if (Os.isFamily(Os.FAMILY_WINDOWS)) { 78 | // This links are used for envs like tox; *nix envs have such links already 79 | link "bin/python2.7.exe", "bin/python.exe", new File(envsDirectory, "django19") 80 | link "bin/python3.4.exe", "bin/python.exe", new File(envsDirectory, "conda34") 81 | } 82 | 83 | //jython "envName", [] 84 | jython "jython" 85 | virtualenv "envJython", "jython", ["django==1.8"] 86 | 87 | if (Os.isFamily(Os.FAMILY_UNIX)) { 88 | //pypy "envName", [] 89 | pypy "pypy2", ["django"] 90 | virtualenv "envPypy2", "pypy2", ["pytest"] 91 | //pypy "envName", "version", [] 92 | //version should be in accordance with python-build 93 | pypy "pypy3", "pypy3.5-5.8.0", ["nose"] 94 | virtualenv "envPypy3", "pypy3", ["django"] 95 | } 96 | 97 | if (Os.isFamily(Os.FAMILY_WINDOWS)) { 98 | //ironpython "envName", [] 99 | ironpython "ironpython64", ["requests"] 100 | //ironpython "envName", "architecture", [] 101 | ironpython "ironpython32", "32", ["requests"] 102 | // ironpython doesn't support virtualenvs at all 103 | } 104 | } 105 | ``` 106 | 107 | Then invoke the `build_envs` task. 108 | 109 | This will download and install specified python's interpreters (python, anaconda and miniconda, jython, pypy, ironpython) to `buildDir/bootstrap`. 110 | 111 | Then it will create several conda and virtual envs in `buildDir/envs`. 112 | 113 | Libraries listed will be installed correspondingly. Packages in list are installed with `pip install` command. If the function `condaPackage()` was called for package name, it will be installed with `conda install` command. It enables to install, for example, PyQt in env. 114 | 115 | 116 | License 117 | ------- 118 | 119 | The code is licensed under the Apache 2.0 License. See the included 120 | [LICENSE](LICENSE) file for details. 121 | 122 | -------------------------------------------------------------------------------- /build.gradle: -------------------------------------------------------------------------------- 1 | plugins { 2 | id 'idea' 3 | id 'groovy' 4 | id 'com.gradle.plugin-publish' version '1.2.1' 5 | id 'com.palantir.git-version' version '3.1.0' 6 | id 'maven-publish' 7 | id 'java-gradle-plugin' 8 | } 9 | 10 | project.version = gitVersion() 11 | project.group = 'com.jetbrains.python' 12 | 13 | java { 14 | sourceCompatibility = JavaVersion.VERSION_21 15 | } 16 | 17 | repositories { 18 | mavenCentral() 19 | } 20 | 21 | publishing { 22 | repositories { 23 | maven { 24 | url uri('../mvn_repo') 25 | } 26 | } 27 | } 28 | 29 | dependencies { 30 | implementation gradleApi() 31 | 32 | testImplementation( 33 | gradleTestKit(), 34 | ) 35 | 36 | testImplementation(platform('org.junit:junit-bom:5.10.3')) 37 | testImplementation('org.junit.jupiter:junit-jupiter') 38 | testRuntimeOnly('org.junit.platform:junit-platform-launcher') 39 | 40 | testImplementation('org.spockframework:spock-core:2.4-M4-groovy-3.0') { 41 | exclude module: 'groovy-all' 42 | } 43 | } 44 | 45 | gradlePlugin { 46 | website = 'https://github.com/JetBrains/gradle-python-envs' 47 | vcsUrl = 'https://github.com/JetBrains/gradle-python-envs' 48 | plugins { 49 | pythonEnvsPlugin { 50 | id = 'com.jetbrains.python.envs' 51 | implementationClass = 'com.jetbrains.python.envs.PythonEnvsPlugin' 52 | displayName = 'Gradle Python Envs plugin' 53 | description = 'Gradle plugin to install different Python environments' 54 | tags.set(['python', 'miniconda', 'conda']) 55 | } 56 | } 57 | } 58 | 59 | test { 60 | useJUnitPlatform() 61 | testLogging { 62 | showCauses = true 63 | showExceptions = true 64 | showStackTraces = true 65 | showStandardStreams = true 66 | 67 | events "started", "passed", "skipped", "failed", "standard_out", "standard_error" 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JetBrains/gradle-python-envs/8f0c2be6752e1b9abcdbf079be9d4f32e5317121/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | distributionBase=GRADLE_USER_HOME 2 | distributionPath=wrapper/dists 3 | distributionUrl=https\://services.gradle.org/distributions/gradle-8.8-bin.zip 4 | networkTimeout=10000 5 | validateDistributionUrl=true 6 | zipStoreBase=GRADLE_USER_HOME 7 | zipStorePath=wrapper/dists 8 | -------------------------------------------------------------------------------- /gradlew: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | # 4 | # Copyright © 2015-2021 the original authors. 5 | # 6 | # Licensed under the Apache License, Version 2.0 (the "License"); 7 | # you may not use this file except in compliance with the License. 8 | # You may obtain a copy of the License at 9 | # 10 | # https://www.apache.org/licenses/LICENSE-2.0 11 | # 12 | # Unless required by applicable law or agreed to in writing, software 13 | # distributed under the License is distributed on an "AS IS" BASIS, 14 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | # See the License for the specific language governing permissions and 16 | # limitations under the License. 17 | # 18 | 19 | ############################################################################## 20 | # 21 | # Gradle start up script for POSIX generated by Gradle. 22 | # 23 | # Important for running: 24 | # 25 | # (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is 26 | # noncompliant, but you have some other compliant shell such as ksh or 27 | # bash, then to run this script, type that shell name before the whole 28 | # command line, like: 29 | # 30 | # ksh Gradle 31 | # 32 | # Busybox and similar reduced shells will NOT work, because this script 33 | # requires all of these POSIX shell features: 34 | # * functions; 35 | # * expansions «$var», «${var}», «${var:-default}», «${var+SET}», 36 | # «${var#prefix}», «${var%suffix}», and «$( cmd )»; 37 | # * compound commands having a testable exit status, especially «case»; 38 | # * various built-in commands including «command», «set», and «ulimit». 39 | # 40 | # Important for patching: 41 | # 42 | # (2) This script targets any POSIX shell, so it avoids extensions provided 43 | # by Bash, Ksh, etc; in particular arrays are avoided. 44 | # 45 | # The "traditional" practice of packing multiple parameters into a 46 | # space-separated string is a well documented source of bugs and security 47 | # problems, so this is (mostly) avoided, by progressively accumulating 48 | # options in "$@", and eventually passing that to Java. 49 | # 50 | # Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS, 51 | # and GRADLE_OPTS) rely on word-splitting, this is performed explicitly; 52 | # see the in-line comments for details. 53 | # 54 | # There are tweaks for specific operating systems such as AIX, CygWin, 55 | # Darwin, MinGW, and NonStop. 56 | # 57 | # (3) This script is generated from the Groovy template 58 | # https://github.com/gradle/gradle/blob/HEAD/platforms/jvm/plugins-application/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt 59 | # within the Gradle project. 60 | # 61 | # You can find Gradle at https://github.com/gradle/gradle/. 62 | # 63 | ############################################################################## 64 | 65 | # Attempt to set APP_HOME 66 | 67 | # Resolve links: $0 may be a link 68 | app_path=$0 69 | 70 | # Need this for daisy-chained symlinks. 71 | while 72 | APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path 73 | [ -h "$app_path" ] 74 | do 75 | ls=$( ls -ld "$app_path" ) 76 | link=${ls#*' -> '} 77 | case $link in #( 78 | /*) app_path=$link ;; #( 79 | *) app_path=$APP_HOME$link ;; 80 | esac 81 | done 82 | 83 | # This is normally unused 84 | # shellcheck disable=SC2034 85 | APP_BASE_NAME=${0##*/} 86 | # Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036) 87 | APP_HOME=$( cd "${APP_HOME:-./}" > /dev/null && pwd -P ) || exit 88 | 89 | # Use the maximum available, or set MAX_FD != -1 to use that value. 90 | MAX_FD=maximum 91 | 92 | warn () { 93 | echo "$*" 94 | } >&2 95 | 96 | die () { 97 | echo 98 | echo "$*" 99 | echo 100 | exit 1 101 | } >&2 102 | 103 | # OS specific support (must be 'true' or 'false'). 104 | cygwin=false 105 | msys=false 106 | darwin=false 107 | nonstop=false 108 | case "$( uname )" in #( 109 | CYGWIN* ) cygwin=true ;; #( 110 | Darwin* ) darwin=true ;; #( 111 | MSYS* | MINGW* ) msys=true ;; #( 112 | NONSTOP* ) nonstop=true ;; 113 | esac 114 | 115 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar 116 | 117 | 118 | # Determine the Java command to use to start the JVM. 119 | if [ -n "$JAVA_HOME" ] ; then 120 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then 121 | # IBM's JDK on AIX uses strange locations for the executables 122 | JAVACMD=$JAVA_HOME/jre/sh/java 123 | else 124 | JAVACMD=$JAVA_HOME/bin/java 125 | fi 126 | if [ ! -x "$JAVACMD" ] ; then 127 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME 128 | 129 | Please set the JAVA_HOME variable in your environment to match the 130 | location of your Java installation." 131 | fi 132 | else 133 | JAVACMD=java 134 | if ! command -v java >/dev/null 2>&1 135 | then 136 | die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 137 | 138 | Please set the JAVA_HOME variable in your environment to match the 139 | location of your Java installation." 140 | fi 141 | fi 142 | 143 | # Increase the maximum file descriptors if we can. 144 | if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then 145 | case $MAX_FD in #( 146 | max*) 147 | # In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked. 148 | # shellcheck disable=SC2039,SC3045 149 | MAX_FD=$( ulimit -H -n ) || 150 | warn "Could not query maximum file descriptor limit" 151 | esac 152 | case $MAX_FD in #( 153 | '' | soft) :;; #( 154 | *) 155 | # In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked. 156 | # shellcheck disable=SC2039,SC3045 157 | ulimit -n "$MAX_FD" || 158 | warn "Could not set maximum file descriptor limit to $MAX_FD" 159 | esac 160 | fi 161 | 162 | # Collect all arguments for the java command, stacking in reverse order: 163 | # * args from the command line 164 | # * the main class name 165 | # * -classpath 166 | # * -D...appname settings 167 | # * --module-path (only if needed) 168 | # * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables. 169 | 170 | # For Cygwin or MSYS, switch paths to Windows format before running java 171 | if "$cygwin" || "$msys" ; then 172 | APP_HOME=$( cygpath --path --mixed "$APP_HOME" ) 173 | CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" ) 174 | 175 | JAVACMD=$( cygpath --unix "$JAVACMD" ) 176 | 177 | # Now convert the arguments - kludge to limit ourselves to /bin/sh 178 | for arg do 179 | if 180 | case $arg in #( 181 | -*) false ;; # don't mess with options #( 182 | /?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath 183 | [ -e "$t" ] ;; #( 184 | *) false ;; 185 | esac 186 | then 187 | arg=$( cygpath --path --ignore --mixed "$arg" ) 188 | fi 189 | # Roll the args list around exactly as many times as the number of 190 | # args, so each arg winds up back in the position where it started, but 191 | # possibly modified. 192 | # 193 | # NB: a `for` loop captures its iteration list before it begins, so 194 | # changing the positional parameters here affects neither the number of 195 | # iterations, nor the values presented in `arg`. 196 | shift # remove old arg 197 | set -- "$@" "$arg" # push replacement arg 198 | done 199 | fi 200 | 201 | 202 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 203 | DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' 204 | 205 | # Collect all arguments for the java command: 206 | # * DEFAULT_JVM_OPTS, JAVA_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments, 207 | # and any embedded shellness will be escaped. 208 | # * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be 209 | # treated as '${Hostname}' itself on the command line. 210 | 211 | set -- \ 212 | "-Dorg.gradle.appname=$APP_BASE_NAME" \ 213 | -classpath "$CLASSPATH" \ 214 | org.gradle.wrapper.GradleWrapperMain \ 215 | "$@" 216 | 217 | # Stop when "xargs" is not available. 218 | if ! command -v xargs >/dev/null 2>&1 219 | then 220 | die "xargs is not available" 221 | fi 222 | 223 | # Use "xargs" to parse quoted args. 224 | # 225 | # With -n1 it outputs one arg per line, with the quotes and backslashes removed. 226 | # 227 | # In Bash we could simply go: 228 | # 229 | # readarray ARGS < <( xargs -n1 <<<"$var" ) && 230 | # set -- "${ARGS[@]}" "$@" 231 | # 232 | # but POSIX shell has neither arrays nor command substitution, so instead we 233 | # post-process each arg (as a line of input to sed) to backslash-escape any 234 | # character that might be a shell metacharacter, then use eval to reverse 235 | # that process (while maintaining the separation between arguments), and wrap 236 | # the whole thing up as a single "set" statement. 237 | # 238 | # This will of course break if any of these variables contains a newline or 239 | # an unmatched quote. 240 | # 241 | 242 | eval "set -- $( 243 | printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" | 244 | xargs -n1 | 245 | sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' | 246 | tr '\n' ' ' 247 | )" '"$@"' 248 | 249 | exec "$JAVACMD" "$@" 250 | -------------------------------------------------------------------------------- /gradlew.bat: -------------------------------------------------------------------------------- 1 | @rem 2 | @rem Copyright 2015 the original author or authors. 3 | @rem 4 | @rem Licensed under the Apache License, Version 2.0 (the "License"); 5 | @rem you may not use this file except in compliance with the License. 6 | @rem You may obtain a copy of the License at 7 | @rem 8 | @rem https://www.apache.org/licenses/LICENSE-2.0 9 | @rem 10 | @rem Unless required by applicable law or agreed to in writing, software 11 | @rem distributed under the License is distributed on an "AS IS" BASIS, 12 | @rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | @rem See the License for the specific language governing permissions and 14 | @rem limitations under the License. 15 | @rem 16 | 17 | @if "%DEBUG%"=="" @echo off 18 | @rem ########################################################################## 19 | @rem 20 | @rem Gradle startup script for Windows 21 | @rem 22 | @rem ########################################################################## 23 | 24 | @rem Set local scope for the variables with windows NT shell 25 | if "%OS%"=="Windows_NT" setlocal 26 | 27 | set DIRNAME=%~dp0 28 | if "%DIRNAME%"=="" set DIRNAME=. 29 | @rem This is normally unused 30 | set APP_BASE_NAME=%~n0 31 | set APP_HOME=%DIRNAME% 32 | 33 | @rem Resolve any "." and ".." in APP_HOME to make it shorter. 34 | for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi 35 | 36 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 37 | set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" 38 | 39 | @rem Find java.exe 40 | if defined JAVA_HOME goto findJavaFromJavaHome 41 | 42 | set JAVA_EXE=java.exe 43 | %JAVA_EXE% -version >NUL 2>&1 44 | if %ERRORLEVEL% equ 0 goto execute 45 | 46 | echo. 1>&2 47 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 1>&2 48 | echo. 1>&2 49 | echo Please set the JAVA_HOME variable in your environment to match the 1>&2 50 | echo location of your Java installation. 1>&2 51 | 52 | goto fail 53 | 54 | :findJavaFromJavaHome 55 | set JAVA_HOME=%JAVA_HOME:"=% 56 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe 57 | 58 | if exist "%JAVA_EXE%" goto execute 59 | 60 | echo. 1>&2 61 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 1>&2 62 | echo. 1>&2 63 | echo Please set the JAVA_HOME variable in your environment to match the 1>&2 64 | echo location of your Java installation. 1>&2 65 | 66 | goto fail 67 | 68 | :execute 69 | @rem Setup the command line 70 | 71 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar 72 | 73 | 74 | @rem Execute Gradle 75 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* 76 | 77 | :end 78 | @rem End local scope for the variables with windows NT shell 79 | if %ERRORLEVEL% equ 0 goto mainEnd 80 | 81 | :fail 82 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of 83 | rem the _cmd.exe /c_ return code! 84 | set EXIT_CODE=%ERRORLEVEL% 85 | if %EXIT_CODE% equ 0 set EXIT_CODE=1 86 | if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE% 87 | exit /b %EXIT_CODE% 88 | 89 | :mainEnd 90 | if "%OS%"=="Windows_NT" endlocal 91 | 92 | :omega 93 | -------------------------------------------------------------------------------- /scripts/publish.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | true 4 | -------------------------------------------------------------------------------- /scripts/verify.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | set -e 4 | 5 | DIR=$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd ) 6 | cd $DIR/.. 7 | 8 | export JAVA_HOME=$JAVA_1_7_HOME 9 | ./gradlew check 10 | -------------------------------------------------------------------------------- /settings.gradle: -------------------------------------------------------------------------------- 1 | rootProject.name = 'gradle-python-envs' 2 | -------------------------------------------------------------------------------- /src/main/groovy/com/jetbrains/python/envs/PythonEnvsExtension.groovy: -------------------------------------------------------------------------------- 1 | package com.jetbrains.python.envs 2 | 3 | 4 | import org.gradle.api.InvalidUserDataException 5 | 6 | /** 7 | * Project extension to configure Python build environment. 8 | * 9 | */ 10 | class PythonEnvsExtension { 11 | File bootstrapDirectory 12 | File envsDirectory 13 | 14 | URL zipRepository 15 | Boolean shouldUseZipsFromRepository = false 16 | 17 | Boolean _64Bits = true // By default 64 bit envs should be installed 18 | String condaDefaultVersion = "Miniconda2-latest" 19 | String pypyDefaultVersion = "pypy2.7-5.8.0" 20 | @SuppressWarnings("unused") 21 | String pipInstallOptions = "--trusted-host pypi.python.org --trusted-host pypi.org --trusted-host files.pythonhosted.org" 22 | 23 | List pythons = [] 24 | List condas = [] 25 | List condaEnvs = [] 26 | List virtualEnvs = [] 27 | List pythonsFromZip = [] 28 | 29 | String CONDA_PREFIX = "CONDA_" 30 | 31 | /** 32 | * @param envName name of environment like "env_for_django" 33 | * @param version py version like "3.4" 34 | * @param packages collection of py packages to install 35 | * @param patchFileUri URI of a patch to apply when building Python (see the `python-build`'s `-p` option). Absolute paths are also accepted. 36 | */ 37 | void python(final String envName, 38 | final String version, 39 | final String architecture = null, 40 | final List packages = null, 41 | final String patchFileUri = null) { 42 | if (zipRepository && shouldUseZipsFromRepository) { 43 | if (patchFileUri) { 44 | throw new InvalidUserDataException("A patch is defined for a pre-built Python") 45 | } 46 | pythonFromZip envName, getUrlFromRepository("python", version, architecture), "python", packages 47 | } else { 48 | pythons << new Python(envName, bootstrapDirectory, EnvType.PYTHON, version, is64(architecture), packages, null, patchFileUri) 49 | } 50 | } 51 | 52 | void python(final String envName, final String version, final List packages) { 53 | python(envName, version, null, packages) 54 | } 55 | 56 | /** 57 | * @see #python 58 | * @param urlToArchive URL link to archive with environment 59 | */ 60 | void pythonFromZip(final String envName, 61 | final URL urlToArchive, 62 | final String type = null, 63 | final List packages = null) { 64 | pythonsFromZip << new Python( 65 | envName, 66 | bootstrapDirectory, 67 | EnvType.fromString(type), 68 | null, 69 | null, 70 | packages, 71 | urlToArchive 72 | ) 73 | } 74 | 75 | /** 76 | * @see #python 77 | * @param sourceEnvName name of inherited environment like "env_for_django" 78 | */ 79 | void virtualenv(final String envName, final String sourceEnvName, final List packages = null) { 80 | Python pythonEnv = (pythons + pythonsFromZip).find { it.name == sourceEnvName } 81 | if (pythonEnv != null) { 82 | virtualEnvs << new VirtualEnv(envName, envsDirectory, pythonEnv, packages) 83 | } else { 84 | println("Specified environment '$sourceEnvName' for virtualenv '$envName' isn't found") 85 | } 86 | } 87 | 88 | /** 89 | * @see #python 90 | */ 91 | void conda(final String envName, 92 | final String version, 93 | final String architecture, 94 | final List packages = null) { 95 | List pipPackages = packages.findAll { !it.startsWith(CONDA_PREFIX) } 96 | List condaPackages = packages.findAll { it.startsWith(CONDA_PREFIX) } 97 | .collect { it.substring(CONDA_PREFIX.length()) } 98 | condas << new Conda(envName, bootstrapDirectory, version, is64(architecture), pipPackages, condaPackages) 99 | } 100 | 101 | void conda(final String envName, 102 | final String version, 103 | final List packages = null) { 104 | conda(envName, version, null, packages) 105 | } 106 | 107 | void conda(final String envName, 108 | final List packages = null) { 109 | conda(envName, condaDefaultVersion, null, packages) 110 | } 111 | 112 | /** 113 | * @see #python 114 | * @param sourceEnvName name of inherited environment like "env_for_django" 115 | */ 116 | void condaenv(final String envName, 117 | final String version, 118 | final String sourceEnvName = null, 119 | final List packages = null) { 120 | List pipPackages = packages.findAll { !it.startsWith(CONDA_PREFIX) } 121 | List condaPackages = packages.findAll { it.startsWith(CONDA_PREFIX) } 122 | .collect { it.substring(CONDA_PREFIX.length()) } 123 | if (sourceEnvName == null) { 124 | conda condaDefaultVersion 125 | } 126 | Conda condaEnv = condas.find { it.name == sourceEnvName ?: condaDefaultVersion } 127 | 128 | if (condaEnv != null) { 129 | condaEnvs << new CondaEnv(envName, envsDirectory, condaEnv, version, pipPackages, condaPackages) 130 | } else { 131 | println("Specified environment '$sourceEnvName' for condaenv '$envName' isn't found") 132 | } 133 | } 134 | 135 | void condaenv(final String envName, 136 | final String version, 137 | final List packages) { 138 | condaenv(envName, version, null, packages) 139 | } 140 | 141 | /** 142 | * @see #python 143 | */ 144 | void jython(final String envName, final List packages = null) { 145 | pythons << new Python(envName, bootstrapDirectory, EnvType.JYTHON, null, null, packages) 146 | } 147 | 148 | /** 149 | * @see #python 150 | */ 151 | void pypy(final String envName, final String version = null, final List packages = null) { 152 | pythons << new Python( 153 | envName, 154 | bootstrapDirectory, 155 | EnvType.PYPY, 156 | version ?: pypyDefaultVersion, 157 | null, 158 | packages 159 | ) 160 | } 161 | 162 | void pypy(final String envName, final List packages) { 163 | pypy(envName, null, packages) 164 | } 165 | 166 | /** 167 | * @see #python 168 | */ 169 | void ironpython(final String envName, 170 | final String architecture = null, 171 | final List packages = null, 172 | final URL urlToArchive = null) { 173 | URL urlToIronPythonZip = new URL("https://github.com/IronLanguages/ironpython2/releases/download/ipy-2.7.9/IronPython.2.7.9.zip") 174 | pythonsFromZip << new Python( 175 | envName, 176 | bootstrapDirectory, 177 | EnvType.IRONPYTHON, 178 | null, 179 | is64(architecture), 180 | packages, 181 | urlToArchive ?: urlToIronPythonZip 182 | ) 183 | } 184 | 185 | void ironpython(final String envName, 186 | final List packages, 187 | final URL urlToArchive = null) { 188 | ironpython(envName, null, packages, urlToArchive) 189 | } 190 | 191 | String condaPackage(final String packageName) { 192 | return CONDA_PREFIX + packageName 193 | } 194 | 195 | private Boolean is64(final String architecture) { 196 | return (architecture == null) ? _64Bits : !(architecture == "32") 197 | } 198 | 199 | private URL getUrlFromRepository(final String type, final String version, final String architecture = null) { 200 | if (!zipRepository) return null 201 | return zipRepository.toURI().resolve("$type-$version-${architecture ?: _64Bits ? "64" : "32"}.zip").toURL() 202 | } 203 | } 204 | 205 | 206 | enum EnvType { 207 | PYTHON, 208 | CONDA, 209 | JYTHON, 210 | PYPY, 211 | IRONPYTHON, 212 | VIRTUALENV 213 | // TODO non-python virtualenv? 214 | 215 | static fromString(String type) { 216 | return type == null ? null : valueOf(type.toUpperCase()) 217 | } 218 | } 219 | 220 | 221 | class Python { 222 | final String name 223 | final File envDir 224 | final EnvType type 225 | final String version 226 | final Boolean is64 227 | final List packages 228 | final URL url 229 | final String patchFileUri 230 | 231 | Python(String name, 232 | File dir, 233 | EnvType type = null, 234 | String version = null, 235 | Boolean is64 = true, 236 | List packages = null, 237 | URL url = null, 238 | String patchFileUri = null) { 239 | this.name = name 240 | this.envDir = new File(dir, name) 241 | this.type = type 242 | this.version = version 243 | this.is64 = is64 244 | this.packages = packages 245 | this.url = url 246 | this.patchFileUri = patchFileUri 247 | } 248 | } 249 | 250 | 251 | class VirtualEnv extends Python { 252 | final Python sourceEnv 253 | 254 | VirtualEnv(String name, File dir, Python sourceEnv, List packages) { 255 | super(name, dir, EnvType.VIRTUALENV, sourceEnv.version, sourceEnv.is64, packages) 256 | this.sourceEnv = sourceEnv 257 | } 258 | } 259 | 260 | 261 | class Conda extends Python { 262 | final List condaPackages 263 | 264 | Conda(String name, 265 | File dir, 266 | String version, 267 | Boolean is64, 268 | List pipPackages, 269 | List condaPackages) { 270 | super(name, dir, EnvType.CONDA, version, is64, pipPackages) 271 | this.condaPackages = condaPackages 272 | } 273 | } 274 | 275 | 276 | class CondaEnv extends Conda { 277 | final Conda sourceEnv 278 | 279 | CondaEnv(String name, 280 | File dir, 281 | Conda sourceEnv, 282 | String version, 283 | List pipPackages, 284 | List condaPackages) { 285 | super(name, dir, version, sourceEnv.is64, pipPackages, condaPackages) 286 | this.sourceEnv = sourceEnv 287 | } 288 | } 289 | 290 | -------------------------------------------------------------------------------- /src/main/groovy/com/jetbrains/python/envs/PythonEnvsPlugin.groovy: -------------------------------------------------------------------------------- 1 | package com.jetbrains.python.envs 2 | 3 | import org.apache.tools.ant.filters.StringInputStream 4 | import org.apache.tools.ant.taskdefs.condition.Os 5 | import org.gradle.api.GradleException 6 | import org.gradle.api.Plugin 7 | import org.gradle.api.Project 8 | import org.gradle.api.Task 9 | import org.gradle.util.VersionNumber 10 | 11 | import java.nio.file.Paths 12 | 13 | class PythonEnvsPlugin implements Plugin { 14 | private static String osName = System.getProperty('os.name').replaceAll(' ', '').with { 15 | return it.contains("Windows") ? "Windows" : it 16 | } 17 | 18 | private static Boolean isWindows = Os.isFamily(Os.FAMILY_WINDOWS) 19 | private static Boolean isUnix = Os.isFamily(Os.FAMILY_UNIX) 20 | private static Boolean isMacOsX = Os.isFamily(Os.FAMILY_MAC) 21 | 22 | private static URL getUrlToDownloadConda(Conda conda) { 23 | final String repository = (conda.version.toLowerCase().contains("miniconda")) ? "miniconda" : "archive" 24 | final String arch = getArch() 25 | final String ext = isWindows ? "exe" : "sh" 26 | 27 | return new URL("https://repo.continuum.io/$repository/${conda.version}-$osName-$arch.$ext") 28 | } 29 | 30 | private static String getArch() { 31 | def arch = System.getProperty("os.arch") 32 | switch (arch) { 33 | case ~/x86|i386|ia-32|i686/: 34 | arch = "x86" 35 | break 36 | case ~/x86_64|amd64|x64|x86-64/: 37 | arch = "x86_64" 38 | break 39 | case ~/arm|arm-v7|armv7|arm32/: 40 | arch = "armv7l" 41 | break 42 | case ~/aarch64|arm64|arm-v8/: 43 | arch = isMacOsX ? "arm64" : "aarch64" 44 | break 45 | } 46 | return arch 47 | } 48 | 49 | private static File getExecutable(String executable, Python env = null, File dir = null, EnvType type = null) { 50 | String pathString 51 | 52 | switch (type ?: env.type) { 53 | case [EnvType.PYTHON, EnvType.CONDA]: 54 | if (executable in ["pip", "virtualenv", "conda"]) { 55 | pathString = isWindows ? "Scripts/${executable}.exe" : "bin/${executable}" 56 | } else if (executable.startsWith("python")) { 57 | pathString = isWindows ? "${executable}.exe" : "bin/${executable}" 58 | } else { 59 | throw new RuntimeException("$executable is not supported for $env.type yet") 60 | } 61 | break 62 | case [EnvType.JYTHON, EnvType.PYPY]: 63 | if (env.type == EnvType.JYTHON && executable == "python") executable = "jython" 64 | pathString = "bin/${executable}${isWindows ? '.exe' : ''}" 65 | break 66 | case EnvType.IRONPYTHON: 67 | if (executable in ["ipy", "python"] ) { 68 | pathString = "net45/${env.is64 ? "ipy.exe" : "ipy32.exe"}" 69 | } else { 70 | pathString = "Scripts/${executable}.exe" 71 | } 72 | break 73 | case EnvType.VIRTUALENV: 74 | pathString = isWindows ? "Scripts/${executable}.exe" : "bin/${executable}" 75 | break 76 | default: 77 | throw new RuntimeException("$env.type env type is not supported yet") 78 | } 79 | 80 | return new File(dir ?: env.envDir, pathString) 81 | } 82 | 83 | private static File getPipFile(Project project) { 84 | new File(project.buildDir, "get-pip.py").with { file -> 85 | if (!file.exists()) { 86 | project.ant.get(dest: file) { 87 | url(url: "https://bootstrap.pypa.io/get-pip.py") 88 | } 89 | } 90 | return file 91 | } 92 | } 93 | 94 | private static Task createInstallPythonBuildTask(Project project, File installDir) { 95 | return project.tasks.create(name: 'install_python_build') { 96 | 97 | onlyIf { 98 | isUnix && !installDir.exists() 99 | } 100 | 101 | doFirst { 102 | project.buildDir.mkdirs() 103 | } 104 | 105 | doLast { 106 | new File(project.buildDir, "pyenv.zip").with { pyenvZip -> 107 | project.logger.quiet("Downloading latest pyenv from github") 108 | project.ant.get(dest: pyenvZip) { 109 | url(url: "https://github.com/pyenv/pyenv/archive/master.zip") 110 | } 111 | 112 | File unzipFolder = new File(project.buildDir, "python-build-tmp") 113 | String pathToPythonBuildInPyenv = "pyenv-master/plugins/python-build" 114 | 115 | project.logger.quiet("Unzipping python-build to $unzipFolder") 116 | project.copy { 117 | from project.zipTree(pyenvZip) 118 | into unzipFolder 119 | include "$pathToPythonBuildInPyenv/**" 120 | eachFile { file -> 121 | file.path = file.path.replaceFirst(pathToPythonBuildInPyenv, '') 122 | } 123 | } 124 | 125 | project.logger.quiet("Installing python-build via bash to $installDir") 126 | project.exec { 127 | commandLine "bash", new File(unzipFolder, "install.sh") 128 | environment PREFIX: installDir 129 | } 130 | 131 | project.logger.quiet("Removing garbage") 132 | unzipFolder.deleteDir() 133 | pyenvZip.delete() 134 | } 135 | } 136 | } 137 | } 138 | 139 | private Task createPythonUnixTask(Project project, Python env) { 140 | 141 | return project.tasks.create(name: "Bootstrap_${env.type}_'$env.name'") { 142 | 143 | dependsOn "install_python_build" 144 | 145 | onlyIf { 146 | isUnix && (!env.envDir.exists() || isPythonInvalid(project, env)) 147 | } 148 | 149 | doFirst { 150 | env.envDir.mkdirs() 151 | env.envDir.deleteDir() 152 | } 153 | 154 | doLast { 155 | project.logger.quiet("Creating $env.type '$env.name' at $env.envDir directory") 156 | try { 157 | project.exec { 158 | executable new File(project.buildDir, "python-build/bin/python-build") 159 | if (env.patchFileUri != null) { 160 | project.logger.quiet("Applying patch from ${env.patchFileUri} to ${env.name}") 161 | if (Paths.get(env.patchFileUri).isAbsolute()) { 162 | standardInput = new FileInputStream(env.patchFileUri) 163 | } 164 | else { 165 | standardInput = new StringInputStream(new URI(env.patchFileUri).toURL().text) 166 | } 167 | args "-p", env.version, env.envDir 168 | } 169 | else { 170 | args env.version, env.envDir 171 | } 172 | } 173 | project.logger.quiet("Successfully") 174 | } 175 | catch (Exception e) { 176 | if (isPythonInvalid(project, env)) { 177 | project.logger.error(e.message) 178 | throw new GradleException(e.message) 179 | } else { 180 | project.logger.warn(e.message) 181 | } 182 | } 183 | 184 | upgradePipAndSetuptools(project, env) 185 | pipInstall(project, env, env.packages) 186 | } 187 | } 188 | } 189 | 190 | private Task createPythonWindowsTask(Project project, Python env) { 191 | return project.tasks.create(name: "Bootstrap_${env.type}_'$env.name'") { 192 | onlyIf { 193 | isWindows && (!env.envDir.exists() || isPythonInvalid(project, env)) 194 | } 195 | 196 | doFirst { 197 | project.buildDir.mkdir() 198 | env.envDir.mkdirs() 199 | env.envDir.deleteDir() 200 | } 201 | 202 | doLast { 203 | project.logger.quiet("Creating $env.type '$env.name' at $env.envDir directory") 204 | 205 | try { 206 | String extension = VersionNumber.parse(env.version) >= VersionNumber.parse("3.5.0") ? "exe" : "msi" 207 | String filename = "python-${env.version}${env.is64 ? (extension == "msi" ? "." : "-") + "amd64" : ""}.$extension" 208 | File installer = new File(project.buildDir, filename) 209 | 210 | project.logger.quiet("Downloading $installer") 211 | project.ant.get(dest: installer) { 212 | url(url: "https://www.python.org/ftp/python/${env.version}/$filename") 213 | } 214 | 215 | project.logger.quiet("Installing $env.name") 216 | if (extension == "msi") { 217 | project.exec { 218 | commandLine "msiexec", "/i", installer, "/quiet", "TARGETDIR=$env.envDir.absolutePath" 219 | } 220 | } else if (extension == "exe") { 221 | project.mkdir(env.envDir) 222 | project.exec { 223 | executable installer 224 | args installer, "/i", "/quiet", "TargetDir=$env.envDir.absolutePath", "Include_launcher=0", 225 | "InstallLauncherAllUsers=0", "Shortcuts=0", "AssociateFiles=0" 226 | } 227 | } 228 | 229 | if (!getExecutable("pip", env).exists()) { 230 | project.logger.quiet("Downloading & installing pip and setuptools") 231 | project.exec { 232 | executable getExecutable("python", env) 233 | args getPipFile(project) 234 | } 235 | } 236 | // It's better to save installer for good uninstall 237 | // installer.delete() 238 | } 239 | catch (Exception e) { 240 | project.logger.error(e.message) 241 | throw new GradleException(e.message) 242 | } 243 | 244 | pipInstall(project, env, env.packages) 245 | } 246 | } 247 | } 248 | 249 | private Task createJythonTask(Project project, Python env) { 250 | return project.tasks.create(name: "Bootstrap_${env.type}_'$env.name'") { 251 | onlyIf { 252 | !env.envDir.exists() || isPythonInvalid(project, env) 253 | } 254 | 255 | doFirst { 256 | env.envDir.deleteDir() 257 | } 258 | 259 | doLast { 260 | project.logger.quiet("Creating $env.type '$env.name' at $env.envDir directory") 261 | 262 | project.javaexec { 263 | main = '-jar' 264 | args project.configurations.jython.singleFile, '-s', '-d', env.envDir, '-t', 'standard' 265 | } 266 | 267 | pipInstall(project, env, env.packages) 268 | } 269 | } 270 | } 271 | 272 | @Override 273 | void apply(Project project) { 274 | PythonEnvsExtension envs = project.extensions.create("envs", PythonEnvsExtension.class) 275 | 276 | project.repositories { 277 | mavenCentral() 278 | } 279 | 280 | project.configurations { 281 | jython 282 | } 283 | 284 | project.afterEvaluate { 285 | project.configurations.jython.incoming.beforeResolve { 286 | project.dependencies { 287 | jython group: 'org.python', name: 'jython-installer', version: '2.7.1' 288 | } 289 | } 290 | 291 | createInstallPythonBuildTask(project, new File(project.buildDir, "python-build")) 292 | 293 | Task python_task = project.tasks.create(name: 'build_pythons') { 294 | 295 | onlyIf { !envs.pythons.empty } 296 | 297 | envs.pythons.each { env -> 298 | switch (env.type) { 299 | case EnvType.PYTHON: 300 | if (isUnix) { 301 | dependsOn createPythonUnixTask(project, env) 302 | } else if (isWindows) { 303 | dependsOn createPythonWindowsTask(project, env) 304 | } else { 305 | project.logger.error("Something is wrong with os: $osName") 306 | } 307 | break 308 | case EnvType.JYTHON: 309 | dependsOn createJythonTask(project, env) 310 | break 311 | case EnvType.PYPY: 312 | if (isUnix) { 313 | dependsOn createPythonUnixTask(project, env) 314 | } else { 315 | project.logger.warn("PyPy installation isn't supported for $osName, please use envFromZip instead") 316 | } 317 | break 318 | default: 319 | project.logger.error("$env.type isn't supported yet") 320 | } 321 | } 322 | } 323 | 324 | Task python_from_zip_task = project.tasks.create(name: 'build_pythons_from_zip') { 325 | 326 | onlyIf { !envs.pythonsFromZip.empty } 327 | 328 | envs.pythonsFromZip.each { env -> 329 | dependsOn project.tasks.create(name: "Bootstrap_${env.type ? env.type : ''}_'$env.name'_from_archive") { 330 | onlyIf { 331 | !env.envDir.exists() || isPythonInvalid(project, env) 332 | } 333 | 334 | doFirst { 335 | project.buildDir.mkdir() 336 | env.envDir.mkdirs() 337 | env.envDir.deleteDir() 338 | } 339 | 340 | doLast { 341 | try { 342 | String archiveName = env.url.toString().with { urlString -> 343 | urlString.substring(urlString.lastIndexOf('/') + 1, urlString.length()) 344 | } 345 | if (!archiveName.endsWith("zip")) { 346 | throw new RuntimeException("Wrong archive extension, only zip is supported") 347 | } 348 | 349 | File zipArchive = new File(project.buildDir, archiveName) 350 | project.logger.quiet("Downloading $archiveName archive from $env.url") 351 | project.ant.get(dest: zipArchive) { 352 | url(url: env.url) 353 | } 354 | 355 | project.logger.quiet("Unzipping downloaded $archiveName archive") 356 | project.ant.unzip(src: zipArchive, dest: env.envDir) 357 | 358 | env.envDir.with { dir -> 359 | if (dir.listFiles().length == 1) { 360 | File intermediateDir = dir.listFiles().last() 361 | if (!intermediateDir.isDirectory()) { 362 | throw new RuntimeException("Archive is wrong, $env.url") 363 | } 364 | project.ant.move(todir: dir) { 365 | fileset(dir: intermediateDir) 366 | } 367 | } else { 368 | return dir 369 | } 370 | } 371 | 372 | if (env.type != null) { 373 | if (!getExecutable("pip", env).exists()) { 374 | project.logger.quiet("Downloading & installing pip and setuptools") 375 | project.exec { 376 | if (env.type == EnvType.IRONPYTHON) { 377 | executable getExecutable("ipy", env) 378 | args "-m", "ensurepip" 379 | } else { 380 | executable getExecutable("python", env) 381 | args getPipFile(project) 382 | } 383 | } 384 | } 385 | upgradePipAndSetuptools(project, env) 386 | } 387 | 388 | project.logger.quiet("Deleting $archiveName archive") 389 | zipArchive.delete() 390 | } 391 | catch (Exception e) { 392 | project.logger.error(e.message) 393 | throw new GradleException(e.message) 394 | } 395 | 396 | pipInstall(project, env, env.packages) 397 | } 398 | } 399 | } 400 | } 401 | 402 | Task virtualenvs_task = project.tasks.create(name: 'build_virtual_envs') { 403 | shouldRunAfter python_task, python_from_zip_task 404 | 405 | onlyIf { !envs.virtualEnvs.empty } 406 | 407 | envs.virtualEnvs.each { env -> 408 | if (env.sourceEnv.type == EnvType.IRONPYTHON) { 409 | project.logger.warn("IronPython doesn't support virtualenvs") 410 | return 411 | } 412 | 413 | dependsOn project.tasks.create("Create_virtualenv_'$env.name'") { 414 | onlyIf { 415 | (!env.envDir.exists() || isPythonInvalid(project, env)) && env.sourceEnv.type != null 416 | } 417 | 418 | doFirst { 419 | env.envDir.mkdirs() 420 | env.envDir.deleteDir() 421 | } 422 | 423 | doLast { 424 | project.logger.quiet("Installing needed virtualenv package") 425 | 426 | pipInstall(project, env.sourceEnv, ["virtualenv"]) 427 | 428 | project.logger.quiet("Creating virtualenv from $env.sourceEnv.name at $env.envDir") 429 | project.exec { 430 | executable getExecutable("virtualenv", env.sourceEnv) 431 | args env.envDir, "--always-copy" 432 | workingDir env.sourceEnv.envDir 433 | } 434 | 435 | pipInstall(project, env, env.packages) 436 | } 437 | } 438 | } 439 | } 440 | 441 | Task conda_task = project.tasks.create(name: "build_condas") { 442 | 443 | onlyIf { !envs.condas.empty } 444 | 445 | envs.condas.each { Conda env -> 446 | dependsOn project.tasks.create(name: "Bootstrap_${env.type}_'$env.name'") { 447 | onlyIf { 448 | !env.envDir.exists() || isPythonInvalid(project, env) 449 | } 450 | 451 | doFirst { 452 | project.buildDir.mkdir() 453 | env.envDir.mkdirs() 454 | env.envDir.deleteDir() 455 | } 456 | 457 | doLast { 458 | URL urlToConda = getUrlToDownloadConda(env) 459 | File installer = new File(project.buildDir, urlToConda.toString().split("/").last()) 460 | 461 | if (!installer.exists()) { 462 | project.logger.quiet("Downloading $installer.name") 463 | project.ant.get(dest: installer) { 464 | url(url: urlToConda) 465 | } 466 | } 467 | 468 | project.logger.quiet("Bootstraping to $env.envDir") 469 | project.exec { 470 | if (isWindows) { 471 | commandLine installer, "/InstallationType=JustMe", "/AddToPath=0", "/RegisterPython=0", "/S", "/D=$env.envDir" 472 | } else { 473 | commandLine "bash", installer, "-b", "-p", env.envDir 474 | } 475 | } 476 | 477 | pipInstall(project, env, env.packages) 478 | condaInstall(project, env, env.condaPackages) 479 | } 480 | } 481 | } 482 | } 483 | 484 | Task conda_envs_task = project.tasks.create(name: 'build_conda_envs') { 485 | shouldRunAfter conda_task 486 | 487 | onlyIf { !envs.condaEnvs.empty } 488 | 489 | envs.condaEnvs.each { env -> 490 | dependsOn project.tasks.create("Create_conda_env_'$env.name'") { 491 | onlyIf { 492 | !env.envDir.exists() || isPythonInvalid(project, env) 493 | } 494 | 495 | doFirst { 496 | env.envDir.mkdirs() 497 | env.envDir.deleteDir() 498 | } 499 | 500 | doLast { 501 | project.logger.quiet("Creating condaenv '$env.name' at $env.envDir directory") 502 | project.exec { 503 | executable getExecutable("conda", env.sourceEnv) 504 | args "create", "-p", env.envDir, "-y", "python=$env.version" 505 | args env.condaPackages 506 | } 507 | 508 | pipInstall(project, env, env.packages) 509 | } 510 | } 511 | } 512 | } 513 | 514 | project.tasks.create(name: 'build_envs') { 515 | dependsOn python_task, 516 | python_from_zip_task, 517 | virtualenvs_task, 518 | conda_task, 519 | conda_envs_task 520 | } 521 | } 522 | } 523 | 524 | private void upgradePipAndSetuptools(Project project, Python env) { 525 | project.logger.quiet("Force upgrade pip and setuptools") 526 | List command = [ 527 | getExecutable("python", env), "-m", "pip", "install", 528 | *project.extensions.findByName("envs").getProperty("pipInstallOptions").split(" "), 529 | "--upgrade", "--force", "pip", "setuptools" 530 | ] 531 | 532 | project.logger.quiet("Executing '${command.join(" ")}'") 533 | if (project.exec { 534 | commandLine command 535 | }.exitValue != 0) throw new GradleException("pip & setuptools upgrade failed") 536 | } 537 | 538 | private void pipInstall(Project project, Python env, List packages) { 539 | if (packages == null || packages.empty || env.type == null) { 540 | return 541 | } 542 | project.logger.quiet("Installing packages via pip: $packages") 543 | 544 | List command = [ 545 | getExecutable("pip", env), 546 | "install", 547 | *project.extensions.findByName("envs").getProperty("pipInstallOptions").split(" "), 548 | *packages 549 | ] 550 | project.logger.quiet("Executing '${command.join(" ")}'") 551 | 552 | if (project.exec { 553 | commandLine command 554 | }.exitValue != 0) throw new GradleException("pip install failed") 555 | } 556 | 557 | private void condaInstall(Project project, Conda conda, List packages) { 558 | if (packages == null || packages.empty) { 559 | return 560 | } 561 | project.logger.quiet("Installing packages via conda: $packages") 562 | 563 | List command = [ 564 | getExecutable("conda", conda), 565 | "install", "-y", 566 | "-p", conda.envDir, 567 | *packages 568 | ] 569 | project.logger.quiet("Executing '${command.join(" ")}'") 570 | 571 | if (project.exec { 572 | commandLine command 573 | }.exitValue != 0) throw new GradleException("conda install failed") 574 | } 575 | 576 | private boolean isPythonValid(Project project, Python env) { 577 | File exec = getExecutable("python", env) 578 | if (!exec.exists()) return false 579 | 580 | int exitValue 581 | try { 582 | exitValue = project.exec { commandLine exec, "-c", "'print(1)'" }.exitValue 583 | } catch (ignored) { 584 | return false 585 | } 586 | 587 | return exitValue == 0 588 | } 589 | 590 | private boolean isPythonInvalid(Project project, Python env) { 591 | return !isPythonValid(project, env) 592 | } 593 | } 594 | -------------------------------------------------------------------------------- /src/test/groovy/com/jetbrains/python/envs/test/FunctionalTest.groovy: -------------------------------------------------------------------------------- 1 | package com.jetbrains.python.envs.test 2 | 3 | import org.gradle.testkit.runner.BuildResult 4 | import org.gradle.testkit.runner.GradleRunner 5 | import spock.lang.TempDir 6 | 7 | import java.nio.file.Files 8 | import java.nio.file.Path 9 | import spock.lang.Requires 10 | import spock.lang.Specification 11 | 12 | import static org.gradle.testkit.runner.TaskOutcome.SUCCESS 13 | import static org.gradle.testkit.runner.TaskOutcome.UP_TO_DATE 14 | 15 | class FunctionalTest extends Specification { 16 | @TempDir 17 | Path testProjectDir 18 | File settingsFile 19 | File buildFile 20 | 21 | def setup() { 22 | settingsFile = Files.createFile(testProjectDir.resolve('settings.gradle')).toFile() 23 | buildFile = Files.createFile(testProjectDir.resolve('build.gradle')).toFile() 24 | } 25 | 26 | def "install python 2.7.15 and virtualenv"() { 27 | given: 28 | settingsFile << "rootProject.name = 'gradle-python-envs'" 29 | buildFile << """ 30 | plugins { 31 | id "com.jetbrains.python.envs" 32 | } 33 | 34 | envs { 35 | bootstrapDirectory = new File(buildDir, 'bootstrap') 36 | envsDirectory = file(buildDir) 37 | 38 | python "python-2.7.15", "2.7.15" 39 | virtualenv "virtualenv-2.7.15", "python-2.7.15" 40 | } 41 | 42 | """ 43 | 44 | when: 45 | BuildResult result = GradleRunner.create() 46 | .withProjectDir(testProjectDir.toFile()) 47 | .withArguments('build_envs') 48 | .withPluginClasspath() 49 | .build() 50 | 51 | then: 52 | println(result.output) 53 | result.output.contains('Installing Python-2.7.15') 54 | result.output.contains('BUILD SUCCESSFUL') 55 | result.task(":build_envs").outcome == SUCCESS 56 | } 57 | 58 | def "install python 3.7.1 and virtualenv"() { 59 | given: 60 | settingsFile << "rootProject.name = 'gradle-python-envs'" 61 | buildFile << """ 62 | plugins { 63 | id "com.jetbrains.python.envs" 64 | } 65 | 66 | envs { 67 | bootstrapDirectory = new File(buildDir, 'bootstrap') 68 | envsDirectory = file(buildDir) 69 | 70 | python "python-3.7.1", "3.7.1" 71 | virtualenv "virtualenv-3.7.1", "python-3.7.1" 72 | } 73 | 74 | """ 75 | 76 | when: 77 | BuildResult result = GradleRunner.create() 78 | .withProjectDir(testProjectDir.toFile()) 79 | .withArguments('build_envs') 80 | .withPluginClasspath() 81 | .build() 82 | 83 | then: 84 | println(result.output) 85 | result.output.contains('Installing Python-3.7.1') 86 | result.output.contains('BUILD SUCCESSFUL') 87 | result.task(":build_envs").outcome == SUCCESS 88 | } 89 | 90 | def "install python 3.6.5 32-bit and virtualenv"() { 91 | given: 92 | settingsFile << "rootProject.name = 'gradle-python-envs'" 93 | buildFile << """ 94 | plugins { 95 | id "com.jetbrains.python.envs" 96 | } 97 | 98 | envs { 99 | bootstrapDirectory = new File(buildDir, 'bootstrap') 100 | envsDirectory = file(buildDir) 101 | 102 | python "python-3.6.5-32", "3.6.5", "32" 103 | virtualenv "virtualenv-3.6.5", "python-3.6.5-32" 104 | } 105 | 106 | """ 107 | 108 | when: 109 | BuildResult result = GradleRunner.create() 110 | .withProjectDir(testProjectDir.toFile()) 111 | .withArguments('build_envs') 112 | .withPluginClasspath() 113 | .build() 114 | 115 | then: 116 | println(result.output) 117 | result.output.contains('Installing Python-3.6.5') 118 | result.output.contains('BUILD SUCCESSFUL') 119 | result.task(":build_envs").outcome == SUCCESS 120 | } 121 | 122 | def "install python 3.5.6 with package"() { 123 | given: 124 | settingsFile << "rootProject.name = 'gradle-python-envs'" 125 | buildFile << """ 126 | plugins { 127 | id "com.jetbrains.python.envs" 128 | } 129 | 130 | envs { 131 | bootstrapDirectory = new File(buildDir, 'bootstrap') 132 | envsDirectory = file(buildDir) 133 | 134 | python "python-3.5.6", "3.5.6", ["django==1.10"] 135 | } 136 | 137 | """ 138 | 139 | when: 140 | BuildResult result = GradleRunner.create() 141 | .withProjectDir(testProjectDir.toFile()) 142 | .withArguments('build_envs') 143 | .withPluginClasspath() 144 | .build() 145 | 146 | then: 147 | println(result.output) 148 | result.output.contains('Installing Python-3.5.6') 149 | result.output.contains('BUILD SUCCESSFUL') 150 | result.task(":build_envs").outcome == SUCCESS 151 | } 152 | 153 | def "install latest miniconda3"() { 154 | given: 155 | settingsFile << "rootProject.name = 'gradle-python-envs'" 156 | buildFile << """ 157 | plugins { 158 | id "com.jetbrains.python.envs" 159 | } 160 | 161 | envs { 162 | bootstrapDirectory = new File(buildDir, 'bootstrap') 163 | envsDirectory = file(buildDir) 164 | 165 | conda "Miniconda3", "Miniconda3-latest" 166 | } 167 | 168 | """ 169 | 170 | when: 171 | BuildResult result = GradleRunner.create() 172 | .withProjectDir(testProjectDir.toFile()) 173 | .withArguments('build_envs') 174 | .withPluginClasspath() 175 | .build() 176 | 177 | then: 178 | println(result.output) 179 | result.output.contains('BUILD SUCCESSFUL') 180 | result.task(":build_envs").outcome == SUCCESS 181 | } 182 | 183 | def "install latest miniconda2"() { 184 | given: 185 | settingsFile << "rootProject.name = 'gradle-python-envs'" 186 | buildFile << """ 187 | plugins { 188 | id "com.jetbrains.python.envs" 189 | } 190 | 191 | envs { 192 | bootstrapDirectory = new File(buildDir, 'bootstrap') 193 | envsDirectory = file(buildDir) 194 | 195 | conda "Miniconda2", "Miniconda2-latest" 196 | } 197 | 198 | """ 199 | 200 | when: 201 | BuildResult result = GradleRunner.create() 202 | .withProjectDir(testProjectDir.toFile()) 203 | .withArguments('build_envs') 204 | .withPluginClasspath() 205 | .build() 206 | 207 | then: 208 | println(result.output) 209 | result.output.contains('BUILD SUCCESSFUL') 210 | result.task(":build_envs").outcome == SUCCESS 211 | } 212 | 213 | def "install anaconda2-5.3.1 64 bit with conda package"() { 214 | given: 215 | settingsFile << "rootProject.name = 'gradle-python-envs'" 216 | buildFile << """ 217 | plugins { 218 | id "com.jetbrains.python.envs" 219 | } 220 | 221 | envs { 222 | bootstrapDirectory = new File(buildDir, 'bootstrap') 223 | envsDirectory = file(buildDir) 224 | 225 | conda "Anaconda2", "Anaconda2-5.3.1", [condaPackage("PyQt")] 226 | } 227 | 228 | """ 229 | 230 | when: 231 | BuildResult result = GradleRunner.create() 232 | .withProjectDir(testProjectDir.toFile()) 233 | .withArguments('build_envs') 234 | .withPluginClasspath() 235 | .build() 236 | 237 | then: 238 | println(result.output) 239 | result.output.contains('BUILD SUCCESSFUL') 240 | result.task(":build_envs").outcome == SUCCESS 241 | } 242 | 243 | def "install anaconda3-5.3.1 with python package"() { 244 | given: 245 | settingsFile << "rootProject.name = 'gradle-python-envs'" 246 | buildFile << """ 247 | plugins { 248 | id "com.jetbrains.python.envs" 249 | } 250 | 251 | envs { 252 | bootstrapDirectory = new File(buildDir, 'bootstrap') 253 | envsDirectory = file(buildDir) 254 | 255 | conda "Anaconda3", "Anaconda3-5.3.1", ["django==1.8"] 256 | } 257 | 258 | """ 259 | 260 | when: 261 | BuildResult result = GradleRunner.create() 262 | .withProjectDir(testProjectDir.toFile()) 263 | .withArguments('build_envs') 264 | .withPluginClasspath() 265 | .build() 266 | 267 | then: 268 | println(result.output) 269 | result.output.contains('BUILD SUCCESSFUL') 270 | result.task(":build_envs").outcome == SUCCESS 271 | } 272 | 273 | def "install condaenv with conda package"() { 274 | given: 275 | settingsFile << "rootProject.name = 'gradle-python-envs'" 276 | buildFile << """ 277 | plugins { 278 | id "com.jetbrains.python.envs" 279 | } 280 | 281 | envs { 282 | bootstrapDirectory = new File(buildDir, 'bootstrap') 283 | envsDirectory = file(buildDir) 284 | 285 | condaenv "pyqt_env", "2.7", [condaPackage("pyqt")] 286 | } 287 | 288 | """ 289 | 290 | when: 291 | BuildResult result = GradleRunner.create() 292 | .withProjectDir(testProjectDir.toFile()) 293 | .withArguments('build_envs') 294 | .withPluginClasspath() 295 | .build() 296 | 297 | then: 298 | println(result.output) 299 | result.output.contains('BUILD SUCCESSFUL') 300 | result.task(":build_envs").outcome == SUCCESS 301 | } 302 | 303 | def "install jython and virtualenv"() { 304 | given: 305 | settingsFile << "rootProject.name = 'gradle-python-envs'" 306 | buildFile << """ 307 | plugins { 308 | id "com.jetbrains.python.envs" 309 | } 310 | 311 | envs { 312 | bootstrapDirectory = new File(buildDir, 'bootstrap') 313 | envsDirectory = file(buildDir) 314 | 315 | jython "jython" 316 | virtualenv "envJython", "jython" 317 | } 318 | 319 | """ 320 | 321 | when: 322 | BuildResult result = GradleRunner.create() 323 | .withProjectDir(testProjectDir.toFile()) 324 | .withArguments('build_envs') 325 | .withPluginClasspath() 326 | .build() 327 | 328 | then: 329 | println(result.output) 330 | result.output.contains('BUILD SUCCESSFUL') 331 | result.task(":build_envs").outcome == SUCCESS 332 | } 333 | 334 | @Requires({ os.isLinux() || os.isMacOs() }) 335 | def "install pypy2 and virtualenv with package"() { 336 | given: 337 | settingsFile << "rootProject.name = 'gradle-python-envs'" 338 | buildFile << """ 339 | plugins { 340 | id "com.jetbrains.python.envs" 341 | } 342 | 343 | envs { 344 | bootstrapDirectory = new File(buildDir, 'bootstrap') 345 | envsDirectory = file(buildDir) 346 | 347 | pypy "pypy2", ["django"] 348 | virtualenv "envPypy2", "pypy2", ["pytest"] 349 | } 350 | 351 | """ 352 | 353 | when: 354 | BuildResult result = GradleRunner.create() 355 | .withProjectDir(testProjectDir.toFile()) 356 | .withArguments('build_envs') 357 | .withPluginClasspath() 358 | .build() 359 | 360 | then: 361 | println(result.output) 362 | result.output.contains('BUILD SUCCESSFUL') 363 | result.task(":build_envs").outcome in [SUCCESS, UP_TO_DATE] 364 | } 365 | 366 | @Requires({ os.isLinux() || os.isMacOs() }) 367 | def "install pypy3 and virtualenv with package"() { 368 | given: 369 | settingsFile << "rootProject.name = 'gradle-python-envs'" 370 | buildFile << """ 371 | plugins { 372 | id "com.jetbrains.python.envs" 373 | } 374 | 375 | envs { 376 | bootstrapDirectory = new File(buildDir, 'bootstrap') 377 | envsDirectory = file(buildDir) 378 | 379 | pypy "pypy3", "pypy3.5-5.8.0", ["nose"] 380 | virtualenv "envPypy3", "pypy3", ["django"] 381 | } 382 | 383 | """ 384 | 385 | when: 386 | BuildResult result = GradleRunner.create() 387 | .withProjectDir(testProjectDir.toFile()) 388 | .withArguments('build_envs') 389 | .withPluginClasspath() 390 | .build() 391 | 392 | then: 393 | println(result.output) 394 | result.output.contains('BUILD SUCCESSFUL') 395 | result.task(":build_envs").outcome in [SUCCESS, UP_TO_DATE] 396 | } 397 | 398 | @Requires({ os.isWindows() }) 399 | def "install ironpython32"() { 400 | given: 401 | settingsFile << "rootProject.name = 'gradle-python-envs'" 402 | buildFile << """ 403 | plugins { 404 | id "com.jetbrains.python.envs" 405 | } 406 | 407 | envs { 408 | bootstrapDirectory = new File(buildDir, 'bootstrap') 409 | envsDirectory = file(buildDir) 410 | 411 | ironpython "ironpython", "32", ["requests"] 412 | } 413 | 414 | """ 415 | 416 | when: 417 | BuildResult result = GradleRunner.create() 418 | .withProjectDir(testProjectDir.toFile()) 419 | .withArguments('build_envs') 420 | .withPluginClasspath() 421 | .build() 422 | 423 | then: 424 | println(result.output) 425 | result.output.contains('Downloading IronPython.2.7.9.zip archive') 426 | result.output.contains('BUILD SUCCESSFUL') 427 | result.task(":build_envs").outcome == SUCCESS 428 | } 429 | 430 | @Requires({ os.isWindows() }) 431 | def "install ironpython64"() { 432 | given: 433 | settingsFile << "rootProject.name = 'gradle-python-envs'" 434 | buildFile << """ 435 | plugins { 436 | id "com.jetbrains.python.envs" 437 | } 438 | 439 | envs { 440 | bootstrapDirectory = new File(buildDir, 'bootstrap') 441 | envsDirectory = file(buildDir) 442 | 443 | ironpython "ironpython64", ["requests"] 444 | } 445 | 446 | """ 447 | 448 | when: 449 | BuildResult result = GradleRunner.create() 450 | .withProjectDir(testProjectDir.toFile()) 451 | .withArguments('build_envs') 452 | .withPluginClasspath() 453 | .build() 454 | 455 | then: 456 | println(result.output) 457 | result.output.contains('Downloading IronPython.2.7.9.zip archive') 458 | result.output.contains('BUILD SUCCESSFUL') 459 | result.task(":build_envs").outcome == SUCCESS 460 | } 461 | 462 | def "test can't patch pre-built python"() { 463 | given: 464 | settingsFile << "rootProject.name = 'gradle-python-envs-test'" 465 | buildFile << """ 466 | plugins { 467 | id "com.jetbrains.python.envs" 468 | } 469 | 470 | apply plugin: 'com.jetbrains.python.envs' 471 | 472 | envs { 473 | bootstrapDirectory = new File(buildDir, 'bootstrap') 474 | envsDirectory = file(buildDir) 475 | 476 | zipRepository = new URL("https://example.com/repo/") 477 | shouldUseZipsFromRepository = true 478 | 479 | python "python-3.10.0", "3.10.0", "64", [], "example.patch" 480 | } 481 | """ 482 | 483 | when: 484 | GradleRunner.create() 485 | .withProjectDir(testProjectDir.toFile()) 486 | .withArguments('build_envs') 487 | .withPluginClasspath() 488 | .build() 489 | 490 | then: 491 | def e = thrown(Exception.class) 492 | e.message.contains("A patch is defined for a pre-built Python") 493 | } 494 | 495 | def "test apply patch"() { 496 | given: 497 | def patchContents ="""Index: README.rst 498 | IDEA additional info: 499 | Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP 500 | <+>UTF-8 501 | =================================================================== 502 | diff --git a/README.rst b/README.rst 503 | --- a/README.rst\t(revision 4641afef661e6a22bc64194bd334b161c95edfe2) 504 | +++ b/README.rst\t(date 1634932856418) 505 | @@ -265,3 +265,5 @@ 506 | code but these are entirely optional. 507 | 508 | All trademarks referenced herein are property of their respective holders. 509 | + 510 | +hello, world 511 | \\ No newline at end of file 512 | 513 | """ 514 | File patchFile = testProjectDir.newFile("example.patch") 515 | patchFile << patchContents 516 | settingsFile << "rootProject.name = 'gradle-python-envs-test'" 517 | buildFile << """ 518 | plugins { 519 | id "com.jetbrains.python.envs" 520 | } 521 | 522 | apply plugin: 'com.jetbrains.python.envs' 523 | 524 | envs { 525 | bootstrapDirectory = new File(buildDir, 'bootstrap') 526 | envsDirectory = file(buildDir) 527 | 528 | python "python-3.10.0", "3.10.0", "64", [], "${patchFile.toURI()}" 529 | } 530 | """ 531 | 532 | when: 533 | BuildResult result = GradleRunner.create() 534 | .withProjectDir(testProjectDir.toFile()) 535 | .withArguments('build_envs') 536 | .withPluginClasspath() 537 | .build() 538 | 539 | then: 540 | println(result.output) 541 | result.output.contains("patching file README.rst") 542 | result.output.contains('BUILD SUCCESSFUL') 543 | result.task(":build_envs").outcome == SUCCESS 544 | } 545 | } 546 | --------------------------------------------------------------------------------