Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Support pyenv on Windows #94

Open
wants to merge 12 commits into
base: main
Choose a base branch
from
15 changes: 7 additions & 8 deletions Jenkinsfile
Original file line number Diff line number Diff line change
Expand Up @@ -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'
}
}
}
Expand Down
2 changes: 1 addition & 1 deletion VERSION
Original file line number Diff line number Diff line change
@@ -1 +1 @@
0.13.6
0.14.0
48 changes: 24 additions & 24 deletions src/com/ableton/Pyenv.groovy
Original file line number Diff line number Diff line change
Expand Up @@ -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
}

/**
Expand All @@ -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(
Expand All @@ -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
Expand All @@ -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',
Expand All @@ -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")) {
ala-ableton marked this conversation as resolved.
Show resolved Hide resolved
script.error "pyenv executable not found in '${pyenvRoot}'"
}
}
}
15 changes: 13 additions & 2 deletions src/com/ableton/VirtualEnv.groovy
Original file line number Diff line number Diff line change
Expand Up @@ -62,22 +62,33 @@ class VirtualEnv implements Serializable {
activateSubDir = 'bin'
} else {
activateSubDir = 'Scripts'
workspace = workspace.replace('\\', '/')
workspace = posixPath(workspace)
}

long seed = randomSeed ?: System.currentTimeMillis() * this.hashCode()
this.venvRootDir = "${workspace}/.venv/venv-${randomName(seed)}"
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}"
}

/**
Expand Down
119 changes: 55 additions & 64 deletions test/com/ableton/PyenvTest.groovy
Original file line number Diff line number Diff line change
Expand Up @@ -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() }
}
Expand All @@ -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)

Expand All @@ -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)

Expand All @@ -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)
}
Expand All @@ -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) {
Expand All @@ -161,30 +120,29 @@ 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
void createVirtualEnvUnsupportedPythonVersion() {
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)
Expand All @@ -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'
}
}
10 changes: 9 additions & 1 deletion test/com/ableton/VirtualEnvTest.groovy
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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))
Expand Down