diff --git a/Jenkinsfile b/Jenkinsfile index 1619a98..bea39ed 100644 --- a/Jenkinsfile +++ b/Jenkinsfile @@ -35,19 +35,18 @@ eventRecorder.timedStage('Integration Test') { nodeLabel = 'generic-mac-xcode12.2' } eventRecorder.timedNode(nodeLabel) { + sh 'env | sort' + echo 'Test VirtualEnv.create' Object venv = virtualenv.create('python3') String venvVersion = venv.run(returnStdout: true, script: 'python --version') assert venvVersion.startsWith('Python 3') - if (isUnix()) { - echo 'Test VirtualEnv.createWithPyenv' - Object pyvenv = pyenv.createVirtualEnv('3.10.3') - String pyvenvVersion = - pyvenv.run(returnStdout: true, script: 'python --version') - echo pyvenvVersion - assert pyvenvVersion.trim() == 'Python 3.10.3' - } + echo 'Test VirtualEnv.createWithPyenv' + Object pyvenv = pyenv.createVirtualEnv('3.10.3') + String pyvenvVersion = pyvenv.run(returnStdout: true, script: 'python --version') + echo pyvenvVersion + assert pyvenvVersion.trim() == 'Python 3.10.3' } } } diff --git a/VERSION b/VERSION index ebf55b3..a803cc2 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -0.13.6 +0.14.0 diff --git a/src/com/ableton/Pyenv.groovy b/src/com/ableton/Pyenv.groovy index f3eb10d..db9559f 100644 --- a/src/com/ableton/Pyenv.groovy +++ b/src/com/ableton/Pyenv.groovy @@ -23,7 +23,7 @@ class Pyenv implements Serializable { Pyenv(Object script, String pyenvRoot) { this.script = script - this.pyenvRoot = pyenvRoot + this.pyenvRoot = pyenvRoot ? VirtualEnv.posixPath(pyenvRoot) : null } /** @@ -44,10 +44,6 @@ class Pyenv implements Serializable { String trimmedPythonVersion = pythonVersion.trim() - if (!script.isUnix()) { - script.error 'This method is not supported on Windows' - } - if (!versionSupported(trimmedPythonVersion)) { script.withEnv(["PYENV_ROOT=${pyenvRoot}"]) { String pyenvVersion = script.sh( @@ -62,19 +58,27 @@ class Pyenv implements Serializable { VirtualEnv venv = new VirtualEnv(script, randomSeed) script.retry(INSTALLATION_RETRIES) { - venv.script.sh( - label: "Install Python version ${trimmedPythonVersion} with pyenv", - script: """ - export PYENV_ROOT=${pyenvRoot} - export PATH=\$PYENV_ROOT/bin:\$PATH - eval "\$(pyenv init --path)" - eval "\$(pyenv init -)" - pyenv install --skip-existing ${trimmedPythonVersion} - pyenv shell ${trimmedPythonVersion} - pip install virtualenv - virtualenv ${venv.venvRootDir} - """, - ) + script.withEnv(["PYENV_VERSION=${trimmedPythonVersion}"]) { + List installCommands = ["export PYENV_ROOT=${pyenvRoot}"] + if (script.isUnix()) { + installCommands += [ + "export PATH=\$PYENV_ROOT/bin:\$PATH", + "eval \"\$(pyenv init --path)\"", + "eval \"\$(pyenv init -)\"", + ] + } else { + installCommands.add("export PATH=${pyenvRoot}/shims:${pyenvRoot}/bin:\$PATH") + } + installCommands += [ + "pyenv install --skip-existing ${trimmedPythonVersion}", + 'pyenv exec pip install virtualenv', + "pyenv exec virtualenv ${venv.venvRootDir}", + ] + venv.script.sh( + label: "Install Python version ${trimmedPythonVersion} with pyenv", + script: installCommands.join('\n') + '\n', + ) + } } return venv @@ -92,10 +96,6 @@ class Pyenv implements Serializable { assertPyenvRoot() boolean result = false - if (!script.isUnix()) { - script.error 'This method is not supported on Windows' - } - script.withEnv(["PYENV_ROOT=${pyenvRoot}"]) { String allVersions = script.sh( label: 'Get Python versions supported by Pyenv', @@ -116,8 +116,8 @@ class Pyenv implements Serializable { protected void assertPyenvRoot() { assert pyenvRoot - if (!script.fileExists(pyenvRoot)) { - script.error "pyenv root path '${pyenvRoot}' does not exist" + if (script.sh(returnStatus: true, script: "${pyenvRoot}/bin/pyenv --version")) { + script.error "pyenv executable not found in '${pyenvRoot}'" } } } diff --git a/src/com/ableton/VirtualEnv.groovy b/src/com/ableton/VirtualEnv.groovy index 695045d..18d5818 100644 --- a/src/com/ableton/VirtualEnv.groovy +++ b/src/com/ableton/VirtualEnv.groovy @@ -62,7 +62,7 @@ class VirtualEnv implements Serializable { activateSubDir = 'bin' } else { activateSubDir = 'Scripts' - workspace = workspace.replace('\\', '/') + workspace = posixPath(workspace) } long seed = randomSeed ?: System.currentTimeMillis() * this.hashCode() @@ -70,14 +70,25 @@ class VirtualEnv implements Serializable { this.venvBinDir = "${venvRootDir}/${activateSubDir}" } + @NonCPS + static final String posixPath(String path) { + if (path[1] == ':') { + return "/${path[0].toLowerCase()}/${path.substring(3).replaceAll('\\\\', '/')}" + } + return path.replaceAll('\\\\', '/') + } + /** * Removes the virtualenv from disk. You can call this method in the cleanup stage of * the pipeline to avoid cluttering the build node with temporary files. Note that the * virtualenv is stored underneath the workspace, so removing the workspace will have * the same effect. + * + * We use `sh` here rather than `deleteDir` because the latter doesn't work with CygWin + * paths on Windows. */ void cleanup() { - script.dir(venvRootDir) { script.deleteDir() } + script.sh "rm -rf ${venvRootDir}" } /** diff --git a/test/com/ableton/PyenvTest.groovy b/test/com/ableton/PyenvTest.groovy index 0844ed1..04273d3 100644 --- a/test/com/ableton/PyenvTest.groovy +++ b/test/com/ableton/PyenvTest.groovy @@ -35,8 +35,8 @@ class PyenvTest extends BasePipelineTest { @Test void assertPyenvRootInvalidRoot() { String pyenvRoot = '/mock/pyenv/root' - helper.registerAllowedMethod('fileExists', [String]) { return false } helper.registerAllowedMethod('isUnix', []) { return true } + helper.addShMock("${pyenvRoot}/bin/pyenv --version", 'pyenv 1.2.3', 0) assertThrows(Exception) { new Pyenv(script, '1.2.3', pyenvRoot).createVirtualEnv() } } @@ -52,19 +52,8 @@ class PyenvTest extends BasePipelineTest { void createVirtualEnv() { String pythonVersion = '1.2.3' String pyenvRoot = '/mock/pyenv/root' - helper.registerAllowedMethod('fileExists', [String]) { return true } helper.registerAllowedMethod('isUnix', []) { return true } - // Indentation must match the actual command - helper.addShMock(""" - export PYENV_ROOT=${pyenvRoot} - export PATH=\$PYENV_ROOT/bin:\$PATH - eval "\$(pyenv init --path)" - eval "\$(pyenv init -)" - pyenv install --skip-existing ${pythonVersion} - pyenv shell ${pythonVersion} - pip install virtualenv - virtualenv /workspace/.venv/${TEST_RANDOM_NAME} - """, '', 0) + helper.addShMock(installCommands(pyenvRoot, pythonVersion), '', 0) helper.addShMock("${pyenvRoot}/bin/pyenv --version", 'pyenv 1.2.3', 0) helper.addShMock("${pyenvRoot}/bin/pyenv install --list", '1.2.3', 0) @@ -79,17 +68,7 @@ class PyenvTest extends BasePipelineTest { String pyenvRoot = '/mock/pyenv/root' helper.registerAllowedMethod('fileExists', [String]) { return true } helper.registerAllowedMethod('isUnix', []) { return true } - // Indentation must match the actual command - helper.addShMock(""" - export PYENV_ROOT=${pyenvRoot} - export PATH=\$PYENV_ROOT/bin:\$PATH - eval "\$(pyenv init --path)" - eval "\$(pyenv init -)" - pyenv install --skip-existing ${pythonVersion} - pyenv shell ${pythonVersion} - pip install virtualenv - virtualenv /workspace/.venv/${TEST_RANDOM_NAME} - """, '', 0) + helper.addShMock(installCommands(pyenvRoot, pythonVersion), '', 1) helper.addShMock("${pyenvRoot}/bin/pyenv --version", 'pyenv 1.2.3', 0) helper.addShMock("${pyenvRoot}/bin/pyenv install --list", '1.2.3', 0) @@ -105,17 +84,7 @@ class PyenvTest extends BasePipelineTest { helper.with { registerAllowedMethod('fileExists', [String]) { return true } registerAllowedMethod('isUnix', []) { return true } - // Indentation must match the actual command - addShMock(""" - export PYENV_ROOT=${pyenvRoot} - export PATH=\$PYENV_ROOT/bin:\$PATH - eval "\$(pyenv init --path)" - eval "\$(pyenv init -)" - pyenv install --skip-existing ${pythonVersion} - pyenv shell ${pythonVersion} - pip install virtualenv - virtualenv /workspace/.venv/${TEST_RANDOM_NAME} - """, '', 1) + addShMock(installCommands(pyenvRoot, pythonVersion), '', 1) addShMock("${pyenvRoot}/bin/pyenv --version", 'pyenv 1.2.3', 0) addShMock("${pyenvRoot}/bin/pyenv install --list", '1.2.3', 0) } @@ -141,17 +110,7 @@ class PyenvTest extends BasePipelineTest { addShMock("${pyenvRoot}/bin/pyenv install --list", '''Available versions: 1.2.3 ''', 0) - // Indentation must match the actual command - addShMock(""" - export PYENV_ROOT=${pyenvRoot} - export PATH=\$PYENV_ROOT/bin:\$PATH - eval "\$(pyenv init --path)" - eval "\$(pyenv init -)" - pyenv install --skip-existing ${pythonVersion} - pyenv shell ${pythonVersion} - pip install virtualenv - virtualenv /workspace/.venv/${TEST_RANDOM_NAME} - """, '', 1) + addShMock(installCommands(pyenvRoot, pythonVersion), '', 1) } assertThrows(Exception) { @@ -161,9 +120,19 @@ class PyenvTest extends BasePipelineTest { @Test void createVirtualEnvWindows() { + String pythonVersion = '1.2.3' + String pyenvRoot = 'C:\\mock\\pyenv\\root' + String cygwinPyenvRoot = '/c/mock/pyenv/root' helper.registerAllowedMethod('isUnix', []) { return false } + helper.addShMock("${cygwinPyenvRoot}/bin/pyenv --version", 'pyenv 1.2.3', 0) + helper.addShMock("${cygwinPyenvRoot}/bin/pyenv install --list", '''Available versions: + 1.2.3 +''', 0) + helper.addShMock(installCommands(cygwinPyenvRoot, pythonVersion, false), '', 0) - assertThrows(Exception) { new Pyenv(script, 'C:\\pyenv').createVirtualEnv('1.2.3') } + Object venv = new Pyenv(script, pyenvRoot).createVirtualEnv(pythonVersion, 1) + + assertEquals("/workspace/.venv/${TEST_RANDOM_NAME}" as String, venv.venvRootDir) } @Test @@ -171,20 +140,9 @@ class PyenvTest extends BasePipelineTest { String pythonVersion = '6.6.6' String pyenvRoot = '/mock/pyenv/root' helper.registerAllowedMethod('error', [String]) { errorCalled = true } - helper.registerAllowedMethod('fileExists', [String]) { return true } helper.registerAllowedMethod('isUnix', []) { return true } helper.addShMock("${pyenvRoot}/bin/pyenv --version", 'pyenv 1.2.3', 0) - // Indentation must match the actual command - helper.addShMock(""" - export PYENV_ROOT=${pyenvRoot} - export PATH=\$PYENV_ROOT/bin:\$PATH - eval "\$(pyenv init --path)" - eval "\$(pyenv init -)" - pyenv install --skip-existing ${pythonVersion} - pyenv shell ${pythonVersion} - pip install virtualenv - virtualenv /workspace/.venv/${TEST_RANDOM_NAME} - """, '', 1) + helper.addShMock(installCommands(pyenvRoot, pythonVersion), '', 1) assertThrows(Exception) { new Pyenv(script, pyenvRoot).createVirtualEnv(pythonVersion, 1) @@ -199,18 +157,51 @@ class PyenvTest extends BasePipelineTest { 2.2.3 2.3.7 ''' - helper.addShMock('/pyenv/bin/pyenv install --list', mockPyenvVersions, 0) - helper.registerAllowedMethod('fileExists', [String]) { return true } + String pyenvRoot = '/pyenv' + helper.addShMock("${pyenvRoot}/bin/pyenv --version", 'pyenv 1.2.3', 0) + helper.addShMock("${pyenvRoot}/bin/pyenv install --list", mockPyenvVersions, 0) helper.registerAllowedMethod('isUnix', []) { return true } - assertTrue(new Pyenv(script, '/pyenv').versionSupported('2.1.3')) - assertFalse(new Pyenv(script, '/pyenv').versionSupported('2.1.3333')) + assertTrue(new Pyenv(script, pyenvRoot).versionSupported('2.1.3')) + assertFalse(new Pyenv(script, pyenvRoot).versionSupported('2.1.3333')) } @Test void versionSupportedWindows() { + // Resembles pyenv's output, at least as of version 2.3.x + String mockPyenvVersions = '''Available versions: + 2.1.3 + 2.2.3 + 2.3.7 +''' + String cygwinPyenvRoot = '/c/pyenv' + helper.addShMock("${cygwinPyenvRoot}/bin/pyenv --version", 'pyenv 1.2.3', 0) + helper.addShMock("${cygwinPyenvRoot}/bin/pyenv install --list", mockPyenvVersions, 0) helper.registerAllowedMethod('isUnix', []) { return false } - assertThrows(Exception) { new Pyenv(script, 'C:\\pyenv').versionSupported('1.2.3') } + assertTrue(new Pyenv(script, cygwinPyenvRoot).versionSupported('2.1.3')) + assertFalse(new Pyenv(script, cygwinPyenvRoot).versionSupported('2.1.3333')) + } + + private String installCommands( + String pyenvRoot, String pythonVersion, boolean isUnix = true + ) { + List installCommands = [ + "export PYENV_ROOT=${pyenvRoot}", + "export PATH=\$PYENV_ROOT/bin:\$PATH", + ] + if (isUnix) { + installCommands += [ + "eval \"\$(pyenv init --path)\"", + "eval \"\$(pyenv init -)\"", + ] + } + installCommands += [ + "pyenv install --skip-existing ${pythonVersion}", + 'pyenv exec pip install virtualenv', + "pyenv exec virtualenv /workspace/.venv/${TEST_RANDOM_NAME}", + ] + + return installCommands.join('\n') + '\n' } } diff --git a/test/com/ableton/VirtualEnvTest.groovy b/test/com/ableton/VirtualEnvTest.groovy index 99b1e28..6a34ced 100644 --- a/test/com/ableton/VirtualEnvTest.groovy +++ b/test/com/ableton/VirtualEnvTest.groovy @@ -87,7 +87,7 @@ class VirtualEnvTest extends BasePipelineTest { assertNotNull(venv) assertNotNull(venv.script) assertNotNull(venv.venvRootDir) - assertEquals("C:/workspace/.venv/${TEST_RANDOM_NAME}" as String, venv.venvRootDir) + assertEquals("/c/workspace/.venv/${TEST_RANDOM_NAME}" as String, venv.venvRootDir) } @Test @@ -124,6 +124,14 @@ class VirtualEnvTest extends BasePipelineTest { assertTrue(exceptionThrown) } + @Test + void posixPath() { + assertEquals('/c/foo/bar', VirtualEnv.posixPath('/c/foo/bar')) + assertEquals('/c/foo/bar', VirtualEnv.posixPath('C:\\foo\\bar')) + assertEquals('foo/bar', VirtualEnv.posixPath('foo\\bar')) + assertEquals('foo/bar', VirtualEnv.posixPath('foo/bar')) + } + @Test void randomName() { assertEquals('58734446', VirtualEnv.randomName(1))