├── scripts ├── publish.sh └── verify.sh ├── settings.gradle ├── .gitignore ├── .travis.yml ├── gradle └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── .github └── workflows │ └── test.yaml ├── gradlew.bat ├── README.md ├── gradlew ├── src ├── main │ └── groovy │ │ └── com │ │ └── jetbrains │ │ └── python │ │ └── envs │ │ ├── PythonEnvsExtension.groovy │ │ └── PythonEnvsPlugin.groovy └── test │ └── groovy │ └── com │ └── jetbrains │ └── python │ └── envs │ └── test │ └── FunctionalTest.groovy └── LICENSE /scripts/publish.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | true 4 | -------------------------------------------------------------------------------- /settings.gradle: -------------------------------------------------------------------------------- 1 | rootProject.name = 'gradle-python-envs' 2 | -------------------------------------------------------------------------------- /.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 -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JetBrains/gradle-python-envs/HEAD/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /.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 -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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. -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 final String PIP_MINIMAL_SUPPORTED_VERSION = "3.9" 23 | 24 | private static URL getUrlToDownloadConda(Conda conda) { 25 | final String repository = (conda.version.toLowerCase().contains("miniconda")) ? "miniconda" : "archive" 26 | final String arch = getArch() 27 | final String ext = isWindows ? "exe" : "sh" 28 | 29 | return new URL("https://repo.continuum.io/$repository/${conda.version}-$osName-$arch.$ext") 30 | } 31 | 32 | private static String getArch() { 33 | def arch = System.getProperty("os.arch") 34 | switch (arch) { 35 | case ~/x86|i386|ia-32|i686/: 36 | arch = "x86" 37 | break 38 | case ~/x86_64|amd64|x64|x86-64/: 39 | arch = "x86_64" 40 | break 41 | case ~/arm|arm-v7|armv7|arm32/: 42 | arch = "armv7l" 43 | break 44 | case ~/aarch64|arm64|arm-v8/: 45 | arch = isMacOsX ? "arm64" : "aarch64" 46 | break 47 | } 48 | return arch 49 | } 50 | 51 | private static File getExecutable(String executable, Python env = null, File dir = null, EnvType type = null) { 52 | String pathString 53 | 54 | switch (type ?: env.type) { 55 | case [EnvType.PYTHON, EnvType.CONDA]: 56 | if (executable in ["pip", "virtualenv", "conda"]) { 57 | pathString = isWindows ? "Scripts/${executable}.exe" : "bin/${executable}" 58 | } else if (executable.startsWith("python")) { 59 | pathString = isWindows ? "${executable}.exe" : "bin/${executable}" 60 | } else { 61 | throw new RuntimeException("$executable is not supported for $env.type yet") 62 | } 63 | break 64 | case [EnvType.JYTHON, EnvType.PYPY]: 65 | if (env.type == EnvType.JYTHON && executable == "python") executable = "jython" 66 | pathString = "bin/${executable}${isWindows ? '.exe' : ''}" 67 | break 68 | case EnvType.IRONPYTHON: 69 | if (executable in ["ipy", "python"] ) { 70 | pathString = "net45/${env.is64 ? "ipy.exe" : "ipy32.exe"}" 71 | } else { 72 | pathString = "Scripts/${executable}.exe" 73 | } 74 | break 75 | case EnvType.VIRTUALENV: 76 | pathString = isWindows ? "Scripts/${executable}.exe" : "bin/${executable}" 77 | break 78 | default: 79 | throw new RuntimeException("$env.type env type is not supported yet") 80 | } 81 | 82 | return new File(dir ?: env.envDir, pathString) 83 | } 84 | 85 | private static File getPipFile(Project project, String versionStr) { 86 | def version = VersionNumber.parse(versionStr) 87 | String name 88 | String remoteUrl 89 | if (version < VersionNumber.parse(PIP_MINIMAL_SUPPORTED_VERSION)) { 90 | // use version-specific script 91 | def shortVersion = "${version.major}.${version.minor}" 92 | name = "get-pip-${shortVersion}.py" 93 | remoteUrl = "https://bootstrap.pypa.io/pip/3.8/get-pip.py" 94 | } else { 95 | name = "get-pip.py" 96 | remoteUrl = "https://bootstrap.pypa.io/get-pip.py" 97 | } 98 | 99 | new File(project.buildDir, name).with { file -> 100 | if (!file.exists()) { 101 | project.ant.get(dest: file) { 102 | url(url: remoteUrl) 103 | } 104 | } 105 | return file 106 | } 107 | } 108 | 109 | private static Task createInstallPythonBuildTask(Project project, File installDir) { 110 | return project.tasks.create(name: 'install_python_build') { 111 | 112 | onlyIf { 113 | isUnix && !installDir.exists() 114 | } 115 | 116 | doFirst { 117 | project.buildDir.mkdirs() 118 | } 119 | 120 | doLast { 121 | new File(project.buildDir, "pyenv.zip").with { pyenvZip -> 122 | project.logger.quiet("Downloading latest pyenv from github") 123 | project.ant.get(dest: pyenvZip) { 124 | url(url: "https://github.com/pyenv/pyenv/archive/master.zip") 125 | } 126 | 127 | File unzipFolder = new File(project.buildDir, "python-build-tmp") 128 | String pathToPythonBuildInPyenv = "pyenv-master/plugins/python-build" 129 | 130 | project.logger.quiet("Unzipping python-build to $unzipFolder") 131 | project.copy { 132 | from project.zipTree(pyenvZip) 133 | into unzipFolder 134 | include "$pathToPythonBuildInPyenv/**" 135 | eachFile { file -> 136 | file.path = file.path.replaceFirst(pathToPythonBuildInPyenv, '') 137 | } 138 | } 139 | 140 | project.logger.quiet("Installing python-build via bash to $installDir") 141 | project.exec { 142 | commandLine "bash", new File(unzipFolder, "install.sh") 143 | environment PREFIX: installDir 144 | } 145 | 146 | project.logger.quiet("Removing garbage") 147 | unzipFolder.deleteDir() 148 | pyenvZip.delete() 149 | } 150 | } 151 | } 152 | } 153 | 154 | private Task createPythonUnixTask(Project project, Python env) { 155 | 156 | return project.tasks.create(name: "Bootstrap_${env.type}_'$env.name'") { 157 | 158 | dependsOn "install_python_build" 159 | 160 | onlyIf { 161 | isUnix && (!env.envDir.exists() || isPythonInvalid(project, env)) 162 | } 163 | 164 | doFirst { 165 | env.envDir.mkdirs() 166 | env.envDir.deleteDir() 167 | } 168 | 169 | doLast { 170 | project.logger.quiet("Creating $env.type '$env.name' at $env.envDir directory") 171 | try { 172 | project.exec { 173 | executable new File(project.buildDir, "python-build/bin/python-build") 174 | if (env.patchFileUri != null) { 175 | project.logger.quiet("Applying patch from ${env.patchFileUri} to ${env.name}") 176 | if (Paths.get(env.patchFileUri).isAbsolute()) { 177 | standardInput = new FileInputStream(env.patchFileUri) 178 | } 179 | else { 180 | standardInput = new StringInputStream(new URI(env.patchFileUri).toURL().text) 181 | } 182 | args "-p", env.version, env.envDir 183 | } 184 | else { 185 | args env.version, env.envDir 186 | } 187 | } 188 | project.logger.quiet("Successfully") 189 | } 190 | catch (Exception e) { 191 | if (isPythonInvalid(project, env)) { 192 | project.logger.error(e.message) 193 | throw new GradleException(e.message) 194 | } else { 195 | project.logger.warn(e.message) 196 | } 197 | } 198 | 199 | upgradePipAndSetuptools(project, env) 200 | pipInstall(project, env, env.packages) 201 | } 202 | } 203 | } 204 | 205 | private Task createPythonWindowsTask(Project project, Python env) { 206 | return project.tasks.create(name: "Bootstrap_${env.type}_'$env.name'") { 207 | onlyIf { 208 | isWindows && (!env.envDir.exists() || isPythonInvalid(project, env)) 209 | } 210 | 211 | doFirst { 212 | project.buildDir.mkdir() 213 | env.envDir.mkdirs() 214 | env.envDir.deleteDir() 215 | } 216 | 217 | doLast { 218 | project.logger.quiet("Creating $env.type '$env.name' at $env.envDir directory") 219 | 220 | try { 221 | String extension = VersionNumber.parse(env.version) >= VersionNumber.parse("3.5.0") ? "exe" : "msi" 222 | String filename = "python-${env.version}${env.is64 ? (extension == "msi" ? "." : "-") + "amd64" : ""}.$extension" 223 | File installer = new File(project.buildDir, filename) 224 | 225 | project.logger.quiet("Downloading $installer") 226 | project.ant.get(dest: installer) { 227 | url(url: "https://www.python.org/ftp/python/${env.version}/$filename") 228 | } 229 | 230 | project.logger.quiet("Installing $env.name") 231 | if (extension == "msi") { 232 | project.exec { 233 | commandLine "msiexec", "/i", installer, "/quiet", "TARGETDIR=$env.envDir.absolutePath" 234 | } 235 | } else if (extension == "exe") { 236 | project.mkdir(env.envDir) 237 | project.exec { 238 | executable installer 239 | args installer, "/i", "/quiet", "TargetDir=$env.envDir.absolutePath", "Include_launcher=0", 240 | "InstallLauncherAllUsers=0", "Shortcuts=0", "AssociateFiles=0" 241 | } 242 | } 243 | 244 | if (!getExecutable("pip", env).exists()) { 245 | project.logger.quiet("Downloading & installing pip and setuptools") 246 | project.exec { 247 | executable getExecutable("python", env) 248 | args getPipFile(project, env.version) 249 | } 250 | } 251 | // It's better to save installer for good uninstall 252 | // installer.delete() 253 | } 254 | catch (Exception e) { 255 | project.logger.error(e.message) 256 | throw new GradleException(e.message) 257 | } 258 | 259 | pipInstall(project, env, env.packages) 260 | } 261 | } 262 | } 263 | 264 | private Task createJythonTask(Project project, Python env) { 265 | return project.tasks.create(name: "Bootstrap_${env.type}_'$env.name'") { 266 | onlyIf { 267 | !env.envDir.exists() || isPythonInvalid(project, env) 268 | } 269 | 270 | doFirst { 271 | env.envDir.deleteDir() 272 | } 273 | 274 | doLast { 275 | project.logger.quiet("Creating $env.type '$env.name' at $env.envDir directory") 276 | 277 | project.javaexec { 278 | main = '-jar' 279 | args project.configurations.jython.singleFile, '-s', '-d', env.envDir, '-t', 'standard' 280 | } 281 | 282 | pipInstall(project, env, env.packages) 283 | } 284 | } 285 | } 286 | 287 | @Override 288 | void apply(Project project) { 289 | PythonEnvsExtension envs = project.extensions.create("envs", PythonEnvsExtension.class) 290 | 291 | project.repositories { 292 | mavenCentral() 293 | } 294 | 295 | project.configurations { 296 | jython 297 | } 298 | 299 | project.afterEvaluate { 300 | project.configurations.jython.incoming.beforeResolve { 301 | project.dependencies { 302 | jython group: 'org.python', name: 'jython-installer', version: '2.7.1' 303 | } 304 | } 305 | 306 | createInstallPythonBuildTask(project, new File(project.buildDir, "python-build")) 307 | 308 | Task python_task = project.tasks.create(name: 'build_pythons') { 309 | 310 | onlyIf { !envs.pythons.empty } 311 | 312 | envs.pythons.each { env -> 313 | switch (env.type) { 314 | case EnvType.PYTHON: 315 | if (isUnix) { 316 | dependsOn createPythonUnixTask(project, env) 317 | } else if (isWindows) { 318 | dependsOn createPythonWindowsTask(project, env) 319 | } else { 320 | project.logger.error("Something is wrong with os: $osName") 321 | } 322 | break 323 | case EnvType.JYTHON: 324 | dependsOn createJythonTask(project, env) 325 | break 326 | case EnvType.PYPY: 327 | if (isUnix) { 328 | dependsOn createPythonUnixTask(project, env) 329 | } else { 330 | project.logger.warn("PyPy installation isn't supported for $osName, please use envFromZip instead") 331 | } 332 | break 333 | default: 334 | project.logger.error("$env.type isn't supported yet") 335 | } 336 | } 337 | } 338 | 339 | Task python_from_zip_task = project.tasks.create(name: 'build_pythons_from_zip') { 340 | 341 | onlyIf { !envs.pythonsFromZip.empty } 342 | 343 | envs.pythonsFromZip.each { env -> 344 | dependsOn project.tasks.create(name: "Bootstrap_${env.type ? env.type : ''}_'$env.name'_from_archive") { 345 | onlyIf { 346 | !env.envDir.exists() || isPythonInvalid(project, env) 347 | } 348 | 349 | doFirst { 350 | project.buildDir.mkdir() 351 | env.envDir.mkdirs() 352 | env.envDir.deleteDir() 353 | } 354 | 355 | doLast { 356 | try { 357 | String archiveName = env.url.toString().with { urlString -> 358 | urlString.substring(urlString.lastIndexOf('/') + 1, urlString.length()) 359 | } 360 | if (!archiveName.endsWith("zip")) { 361 | throw new RuntimeException("Wrong archive extension, only zip is supported") 362 | } 363 | 364 | File zipArchive = new File(project.buildDir, archiveName) 365 | project.logger.quiet("Downloading $archiveName archive from $env.url") 366 | project.ant.get(dest: zipArchive) { 367 | url(url: env.url) 368 | } 369 | 370 | project.logger.quiet("Unzipping downloaded $archiveName archive") 371 | project.ant.unzip(src: zipArchive, dest: env.envDir) 372 | 373 | env.envDir.with { dir -> 374 | if (dir.listFiles().length == 1) { 375 | File intermediateDir = dir.listFiles().last() 376 | if (!intermediateDir.isDirectory()) { 377 | throw new RuntimeException("Archive is wrong, $env.url") 378 | } 379 | project.ant.move(todir: dir) { 380 | fileset(dir: intermediateDir) 381 | } 382 | } else { 383 | return dir 384 | } 385 | } 386 | 387 | if (env.type != null) { 388 | if (!getExecutable("pip", env).exists()) { 389 | project.logger.quiet("Downloading & installing pip and setuptools") 390 | project.exec { 391 | if (env.type == EnvType.IRONPYTHON) { 392 | executable getExecutable("ipy", env) 393 | args "-m", "ensurepip" 394 | } else { 395 | executable getExecutable("python", env) 396 | args getPipFile(project, env.version) 397 | } 398 | } 399 | } 400 | upgradePipAndSetuptools(project, env) 401 | } 402 | 403 | project.logger.quiet("Deleting $archiveName archive") 404 | zipArchive.delete() 405 | } 406 | catch (Exception e) { 407 | project.logger.error(e.message) 408 | throw new GradleException(e.message) 409 | } 410 | 411 | pipInstall(project, env, env.packages) 412 | } 413 | } 414 | } 415 | } 416 | 417 | Task virtualenvs_task = project.tasks.create(name: 'build_virtual_envs') { 418 | shouldRunAfter python_task, python_from_zip_task 419 | 420 | onlyIf { !envs.virtualEnvs.empty } 421 | 422 | envs.virtualEnvs.each { env -> 423 | if (env.sourceEnv.type == EnvType.IRONPYTHON) { 424 | project.logger.warn("IronPython doesn't support virtualenvs") 425 | return 426 | } 427 | 428 | dependsOn project.tasks.create("Create_virtualenv_'$env.name'") { 429 | onlyIf { 430 | (!env.envDir.exists() || isPythonInvalid(project, env)) && env.sourceEnv.type != null 431 | } 432 | 433 | doFirst { 434 | env.envDir.mkdirs() 435 | env.envDir.deleteDir() 436 | } 437 | 438 | doLast { 439 | project.logger.quiet("Installing needed virtualenv package") 440 | 441 | pipInstall(project, env.sourceEnv, ["virtualenv"]) 442 | 443 | project.logger.quiet("Creating virtualenv from $env.sourceEnv.name at $env.envDir") 444 | project.exec { 445 | executable getExecutable("virtualenv", env.sourceEnv) 446 | args env.envDir, "--always-copy" 447 | workingDir env.sourceEnv.envDir 448 | } 449 | 450 | pipInstall(project, env, env.packages) 451 | } 452 | } 453 | } 454 | } 455 | 456 | Task conda_task = project.tasks.create(name: "build_condas") { 457 | 458 | onlyIf { !envs.condas.empty } 459 | 460 | envs.condas.each { Conda env -> 461 | dependsOn project.tasks.create(name: "Bootstrap_${env.type}_'$env.name'") { 462 | onlyIf { 463 | !env.envDir.exists() || isPythonInvalid(project, env) 464 | } 465 | 466 | doFirst { 467 | project.buildDir.mkdir() 468 | env.envDir.mkdirs() 469 | env.envDir.deleteDir() 470 | } 471 | 472 | doLast { 473 | URL urlToConda = getUrlToDownloadConda(env) 474 | File installer = new File(project.buildDir, urlToConda.toString().split("/").last()) 475 | 476 | if (!installer.exists()) { 477 | project.logger.quiet("Downloading $installer.name") 478 | project.ant.get(dest: installer) { 479 | url(url: urlToConda) 480 | } 481 | } 482 | 483 | project.logger.quiet("Bootstraping to $env.envDir") 484 | project.exec { 485 | if (isWindows) { 486 | commandLine installer, "/InstallationType=JustMe", "/AddToPath=0", "/RegisterPython=0", "/S", "/D=$env.envDir" 487 | } else { 488 | commandLine "bash", installer, "-b", "-p", env.envDir 489 | } 490 | } 491 | 492 | pipInstall(project, env, env.packages) 493 | condaInstall(project, env, env.condaPackages) 494 | } 495 | } 496 | } 497 | } 498 | 499 | Task conda_envs_task = project.tasks.create(name: 'build_conda_envs') { 500 | shouldRunAfter conda_task 501 | 502 | onlyIf { !envs.condaEnvs.empty } 503 | 504 | envs.condaEnvs.each { env -> 505 | dependsOn project.tasks.create("Create_conda_env_'$env.name'") { 506 | onlyIf { 507 | !env.envDir.exists() || isPythonInvalid(project, env) 508 | } 509 | 510 | doFirst { 511 | env.envDir.mkdirs() 512 | env.envDir.deleteDir() 513 | } 514 | 515 | doLast { 516 | project.logger.quiet("Creating condaenv '$env.name' at $env.envDir directory") 517 | project.exec { 518 | executable getExecutable("conda", env.sourceEnv) 519 | args "create", "-p", env.envDir, "-y", "python=$env.version" 520 | args env.condaPackages 521 | } 522 | 523 | pipInstall(project, env, env.packages) 524 | } 525 | } 526 | } 527 | } 528 | 529 | project.tasks.create(name: 'build_envs') { 530 | dependsOn python_task, 531 | python_from_zip_task, 532 | virtualenvs_task, 533 | conda_task, 534 | conda_envs_task 535 | } 536 | } 537 | } 538 | 539 | private void upgradePipAndSetuptools(Project project, Python env) { 540 | project.logger.quiet("Force upgrade pip and setuptools") 541 | List command = [ 542 | getExecutable("python", env), "-m", "pip", "install", 543 | *project.extensions.findByName("envs").getProperty("pipInstallOptions").split(" "), 544 | "--upgrade", "--force", "pip", "setuptools" 545 | ] 546 | 547 | project.logger.quiet("Executing '${command.join(" ")}'") 548 | if (project.exec { 549 | commandLine command 550 | }.exitValue != 0) throw new GradleException("pip & setuptools upgrade failed") 551 | } 552 | 553 | private void pipInstall(Project project, Python env, List packages) { 554 | if (packages == null || packages.empty || env.type == null) { 555 | return 556 | } 557 | project.logger.quiet("Installing packages via pip: $packages") 558 | 559 | List command = [ 560 | getExecutable("pip", env), 561 | "install", 562 | *project.extensions.findByName("envs").getProperty("pipInstallOptions").split(" "), 563 | *packages 564 | ] 565 | project.logger.quiet("Executing '${command.join(" ")}'") 566 | 567 | if (project.exec { 568 | commandLine command 569 | }.exitValue != 0) throw new GradleException("pip install failed") 570 | } 571 | 572 | private void condaInstall(Project project, Conda conda, List packages) { 573 | if (packages == null || packages.empty) { 574 | return 575 | } 576 | project.logger.quiet("Installing packages via conda: $packages") 577 | 578 | List command = [ 579 | getExecutable("conda", conda), 580 | "install", "-y", 581 | "-p", conda.envDir, 582 | *packages 583 | ] 584 | project.logger.quiet("Executing '${command.join(" ")}'") 585 | 586 | if (project.exec { 587 | commandLine command 588 | }.exitValue != 0) throw new GradleException("conda install failed") 589 | } 590 | 591 | private boolean isPythonValid(Project project, Python env) { 592 | File exec = getExecutable("python", env) 593 | if (!exec.exists()) return false 594 | 595 | int exitValue 596 | try { 597 | exitValue = project.exec { commandLine exec, "-c", "'print(1)'" }.exitValue 598 | } catch (ignored) { 599 | return false 600 | } 601 | 602 | return exitValue == 0 603 | } 604 | 605 | private boolean isPythonInvalid(Project project, Python env) { 606 | return !isPythonValid(project, env) 607 | } 608 | } 609 | --------------------------------------------------------------------------------