diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..6451a39
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,5 @@
+/target/
+.classpath
+.project
+/bin/
+.settings
diff --git a/.project b/.project
new file mode 100644
index 0000000..f0a305b
--- /dev/null
+++ b/.project
@@ -0,0 +1,29 @@
+
+
+ python
+
+
+
+
+
+ org.eclipse.jdt.core.javabuilder
+
+
+
+
+ net.sf.eclipsecs.core.CheckstyleBuilder
+
+
+
+
+ org.eclipse.m2e.core.maven2Builder
+
+
+
+
+
+ org.eclipse.jdt.core.javanature
+ org.eclipse.m2e.core.maven2Nature
+ net.sf.eclipsecs.core.CheckstyleNature
+
+
diff --git a/findbugs-exclude.xml b/findbugs-exclude.xml
new file mode 100644
index 0000000..32deffd
--- /dev/null
+++ b/findbugs-exclude.xml
@@ -0,0 +1,10 @@
+
+
+
+
+
+
+
+
+
+
diff --git a/pom.xml b/pom.xml
index 385fab8..437f2e6 100644
--- a/pom.xml
+++ b/pom.xml
@@ -3,7 +3,7 @@
org.jenkins-ci.pluginsplugin
- 1.565.1
+ 1.576
@@ -41,5 +41,167 @@
http://repo.jenkins-ci.org/public/
+
+
+
+
+
+ org.codehaus.mojo
+ cobertura-maven-plugin
+
+
+
+
+ **/Messages.class
+
+
+
+
+
+
+ org.apache.maven.plugins
+ maven-surefire-plugin
+
+ always
+
+
+
+ org.apache.maven.plugins
+ maven-compiler-plugin
+
+ 1.6
+ 1.6
+
+
+
+ org.apache.maven.plugins
+ maven-checkstyle-plugin
+ 2.11
+
+ swe_checkstyle.xml
+ checkstyle-suppressions.xml
+ checkstyle.suppressions.file
+ true
+ true
+
+
+
+
+ checkstyle
+
+ compile
+
+
+ test-check
+
+ check
+
+ test
+
+ warning
+
+
+
+
+
+ org.codehaus.mojo
+ findbugs-maven-plugin
+ 3.0.3
+
+ true
+ findbugs-exclude.xml
+
+
+
+
+ findbugs
+
+ test
+
+
+
+
+
+
+ org.codehaus.gmaven
+ gmaven-plugin
+
+ 1.8
+
+
+
+
+
+
+
+
+ org.apache.maven.plugins
+ maven-checkstyle-plugin
+ 2.11
+
+ swe_checkstyle.xml
+ true
+
+
+
+ org.codehaus.mojo
+ findbugs-maven-plugin
+ 2.4.0
+
+ true
+ findbugs-exclude.xml
+
+
+
+
+
+
+ UTF-8
+
+
+
+
+
+ org.apache.commons
+ commons-exec
+ 1.2
+
+
+ org.hamcrest
+ hamcrest-library
+ 1.3
+ test
+
+
+ org.easymock
+ easymock
+ 3.3.1
+
+
+ cglib
+ cglib-nodep
+
+
+ test
+
+
+ com.google.dexmaker
+ dexmaker
+ 1.1
+ test
+
+
+ org.powermock
+ powermock-api-easymock
+ 1.6.2
+ test
+
+
+ org.powermock
+ powermock-module-junit4
+ 1.6.2
+ test
+
+
diff --git a/src/main/java/hudson/plugins/python/AbstractPythonScript.java b/src/main/java/hudson/plugins/python/AbstractPythonScript.java
new file mode 100644
index 0000000..e82b340
--- /dev/null
+++ b/src/main/java/hudson/plugins/python/AbstractPythonScript.java
@@ -0,0 +1,283 @@
+/*
+ * The MIT License
+ *
+ * Copyright (c) 2016 Markus Winter. All rights reserved.
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in
+ * all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+ * THE SOFTWARE.
+ */
+package hudson.plugins.python;
+
+import java.io.IOException;
+import java.io.Serializable;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Map;
+import java.util.concurrent.atomic.AtomicInteger;
+import java.util.logging.Logger;
+
+import org.apache.commons.collections.CollectionUtils;
+import org.apache.commons.exec.CommandLine;
+import org.apache.commons.lang.StringUtils;
+
+import hudson.DescriptorExtensionList;
+import hudson.EnvVars;
+import hudson.FilePath;
+import hudson.Launcher;
+import hudson.Util;
+import hudson.model.AbstractBuild;
+import hudson.model.AbstractProject;
+import hudson.model.BuildListener;
+import hudson.model.Computer;
+import hudson.model.Descriptor;
+import hudson.plugins.python.installations.AbstractPythonInstallation;
+import hudson.plugins.python.installations.PythonExecutable;
+import hudson.plugins.python.installations.PythonLocator;
+import hudson.tasks.BuildStepDescriptor;
+import hudson.tasks.Builder;
+import hudson.util.VariableResolver;
+
+public abstract class AbstractPythonScript extends Builder implements Serializable
+{
+ private static final long serialVersionUID = -3956021436132554560L;
+
+ protected String pythonName;
+ protected String scriptArguments;
+ protected ScriptSource scriptSource;
+
+ protected boolean quiet;
+
+ protected String options;
+
+ private static Logger logger = Logger.getLogger(AbstractPythonScript.class.getName());
+
+
+ public AbstractPythonScript(String pythonName, ScriptSource scriptSource, String scriptArgs, String options,
+ boolean quiet)
+ {
+ this.pythonName = pythonName;
+ this.scriptArguments = scriptArgs;
+ this.scriptSource = scriptSource;
+ this.options = options;
+ this.quiet = quiet;
+ }
+
+
+ public boolean getQuiet()
+ {
+ return quiet;
+ }
+
+
+ public String getOptions()
+ {
+ return options;
+ }
+
+
+ public String getPythonName()
+ {
+ return pythonName;
+ }
+
+
+ public String getScriptArgs()
+ {
+ return scriptArguments;
+ }
+
+
+ public ScriptSource getScriptSource()
+ {
+ return scriptSource;
+ }
+
+
+ @Override
+ public boolean perform(AbstractBuild, ?> build, Launcher launcher, BuildListener listener)
+ throws InterruptedException, IOException
+ {
+
+ if (scriptSource == null)
+ {
+ listener.fatalError("There is no script configured for this builder");
+ return false;
+ }
+
+ FilePath ws = build.getWorkspace();
+ FilePath script = null;
+ try
+ {
+ script = scriptSource.getScriptFile(ws, build, listener);
+ }
+ catch (IOException e)
+ {
+ Util.displayIOException(e, listener);
+ e.printStackTrace(listener.fatalError("Unable to produce a script file"));
+ return false;
+ }
+
+ try
+ {
+ List cmd = buildCommandLine(build, listener, launcher, script);
+ int result;
+ try
+ {
+ Map envVars = build.getEnvironment(listener);
+
+ for (Map.Entry e : build.getBuildVariables().entrySet())
+ {
+ envVars.put(e.getKey(), e.getValue());
+ }
+
+ envVars.put("$PATH_SEPARATOR", ":::"); // TODO why??
+
+ result = launcher.launch().cmds(cmd.toArray(new String[] { })).envs(envVars).stdout(listener).pwd(ws)
+ .quiet(quiet).join();
+ }
+ catch (IOException e)
+ {
+ Util.displayIOException(e, listener);
+ e.printStackTrace(listener.fatalError("command execution failed"));
+ result = -1;
+ }
+ return result == 0;
+ }
+ finally
+ {
+ try
+ {
+ if ((scriptSource instanceof StringSource) && (script != null))
+ {
+ script.delete();
+ }
+ }
+ catch (IOException e)
+ {
+ Util.displayIOException(e, listener);
+ e.printStackTrace(listener.fatalError("Unable to delete script file " + script));
+ }
+ }
+ }
+
+
+ protected abstract T getPython();
+
+
+ @SuppressWarnings("unchecked")
+ protected List buildCommandLine(AbstractBuild, ?> build, BuildListener listener, Launcher launcher,
+ FilePath script) throws IOException, InterruptedException
+ {
+ ArrayList list = new ArrayList();
+
+ EnvVars env = build.getEnvironment(listener);
+ env.overrideAll(build.getBuildVariables());
+
+ PythonExecutable cmd = null;
+
+ T installation = getPython();
+ if (installation != null)
+ {
+ installation = (T)installation.forNode(Computer.currentComputer().getNode(), listener);
+ installation = (T)installation.forEnvironment(env);
+ cmd = installation.getExecutable(launcher);
+ }
+
+ if (cmd == null)
+ {
+ PythonLocator locator = new PythonLocator(launcher);
+ PythonExecutable pythonExe = locator.findPythonForVersion(getRequiredPythonVersion());
+ if (pythonExe == null)
+ {
+ throw new IOException(
+ "No Python found that matches the requested python version " + getRequiredPythonVersion());
+ }
+ cmd = pythonExe;
+ logger.fine(
+ "[Python WARNING] Python executable is not configured, please check your Python configuration.");
+ logger.fine("[Python WARNING] Using python found at " + cmd.getExecutable());
+ }
+
+ list.add(cmd.getExecutable());
+ list.addAll(cmd.getArguments());
+
+ list.addAll(parseArgumentsAndOptions(options));
+ list.add(script.getRemote());
+
+ if (StringUtils.isNotBlank(scriptArguments))
+ {
+ VariableResolver evr = new VariableResolver.ByMap(env);
+ VariableResolver pvr = build.getBuildVariableResolver();
+ List params = parseArgumentsAndOptions(scriptArguments);
+ for (String param : params)
+ {
+ String p = Util.replaceMacro(param, evr);
+ p = Util.replaceMacro(p, pvr);
+ list.add(p);
+ }
+ }
+
+ return list;
+ }
+
+
+ protected abstract int getRequiredPythonVersion();
+
+
+ private List parseArgumentsAndOptions(String line)
+ {
+ CommandLine cmdLine = CommandLine.parse("dummy_executable " + line);
+ List args = new ArrayList();
+ CollectionUtils.addAll(args, cmdLine.getArguments());
+ return args;
+ }
+
+ public abstract static class AbstractPythonScriptDescriptor extends BuildStepDescriptor
+ {
+
+ public abstract T[] getInstallations();
+
+
+ public AbstractPythonScriptDescriptor(Class extends Builder> clazz)
+ {
+ super(clazz);
+ }
+
+ private AtomicInteger instanceCounter = new AtomicInteger(0);
+
+
+ public int nextInstanceID()
+ {
+ return instanceCounter.incrementAndGet();
+ }
+
+
+ @Override
+ @SuppressWarnings("rawtypes")
+ public boolean isApplicable(Class extends AbstractProject> jobType)
+ {
+ return true;
+ }
+
+
+ public static DescriptorExtensionList> getScriptSources()
+ {
+ return ScriptSource.all();
+ }
+ }
+}
diff --git a/src/main/java/hudson/plugins/python/FileScriptSource.java b/src/main/java/hudson/plugins/python/FileScriptSource.java
new file mode 100644
index 0000000..e577a00
--- /dev/null
+++ b/src/main/java/hudson/plugins/python/FileScriptSource.java
@@ -0,0 +1,75 @@
+/*
+ * The MIT License
+ *
+ * Copyright (c) 2016 Markus Winter. All rights reserved.
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in
+ * all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+ * THE SOFTWARE.
+ */
+package hudson.plugins.python;
+
+import java.io.IOException;
+
+import org.kohsuke.stapler.DataBoundConstructor;
+
+import hudson.EnvVars;
+import hudson.Extension;
+import hudson.FilePath;
+import hudson.model.AbstractBuild;
+import hudson.model.BuildListener;
+import hudson.model.Descriptor;
+
+public class FileScriptSource extends ScriptSource
+{
+
+ private String scriptFile;
+
+
+ @DataBoundConstructor
+ public FileScriptSource(String scriptFile)
+ {
+ this.scriptFile = scriptFile;
+ }
+
+
+ public String getScriptFile()
+ {
+ return scriptFile;
+ }
+
+
+ @Override
+ public FilePath getScriptFile(FilePath workspace, AbstractBuild, ?> build, BuildListener listener)
+ throws IOException, InterruptedException
+ {
+ EnvVars env = build.getEnvironment(listener);
+ String expandedScriptdFile = env.expand(this.scriptFile);
+ return new FilePath(workspace, expandedScriptdFile);
+ }
+
+ @Extension
+ public static class DescriptorImpl extends Descriptor
+ {
+
+ @Override
+ public String getDisplayName()
+ {
+ return "Python script file";
+ }
+ }
+}
diff --git a/src/main/java/hudson/plugins/python/Python.java b/src/main/java/hudson/plugins/python/Python.java
deleted file mode 100644
index 08b02a0..0000000
--- a/src/main/java/hudson/plugins/python/Python.java
+++ /dev/null
@@ -1,46 +0,0 @@
-package hudson.plugins.python;
-
-import hudson.Extension;
-import hudson.FilePath;
-import hudson.model.Descriptor;
-import hudson.tasks.Builder;
-import hudson.tasks.CommandInterpreter;
-import net.sf.json.JSONObject;
-import org.kohsuke.stapler.DataBoundConstructor;
-import org.kohsuke.stapler.StaplerRequest;
-
-/**
- * Invokes the Python interpreter and invokes the Python script entered on the
- * hudson build configuration.
- *
- * It is expected that the Python interpreter is available on the system PATH.
- *
- */
-public class Python extends CommandInterpreter {
- @DataBoundConstructor
- public Python(String command) {
- super(command);
- }
-
- @Override
- public String[] buildCommandLine(FilePath script) {
- return new String[]{"python", script.getRemote()};
- }
-
- @Override
- protected String getContents() {
- return command;
- }
-
- @Override
- protected String getFileExtension() {
- return ".py";
- }
-
- @Extension
- public static final class DescriptorImpl extends Descriptor {
- public String getDisplayName() {
- return Messages.Python_DisplayName();
- }
- }
-}
diff --git a/src/main/java/hudson/plugins/python/Python2Script.java b/src/main/java/hudson/plugins/python/Python2Script.java
new file mode 100644
index 0000000..7b52a8e
--- /dev/null
+++ b/src/main/java/hudson/plugins/python/Python2Script.java
@@ -0,0 +1,132 @@
+/*
+ * The MIT License
+ *
+ * Copyright (c) 2016 Markus Winter. All rights reserved.
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in
+ * all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+ * THE SOFTWARE.
+ */
+package hudson.plugins.python;
+
+import static hudson.init.InitMilestone.PLUGINS_STARTED;
+
+import java.io.File;
+
+import org.kohsuke.stapler.DataBoundConstructor;
+
+import hudson.Extension;
+import hudson.init.Initializer;
+import hudson.model.Items;
+import hudson.plugins.python.installations.Python2Installation;
+import jenkins.model.Jenkins;
+import net.sf.json.JSONObject;
+
+public class Python2Script extends AbstractPythonScript
+{
+
+ private static final long serialVersionUID = 7102426938537145769L;
+
+ protected transient String command;
+
+
+ @DataBoundConstructor
+ public Python2Script(String pythonName, ScriptSource scriptSource, JSONObject python2Options, String scriptArgs,
+ String options, boolean quiet)
+ {
+ super(pythonName, scriptSource, scriptArgs, options, quiet);
+ }
+
+
+ @Override
+ protected Python2Installation getPython()
+ {
+ for (Python2Installation i : Jenkins.getInstance()
+ .getDescriptorByType(Python2Installation.Python2InstallationDescriptor.class)
+ .getInstallations())
+ {
+ if (pythonName != null && pythonName.equals(i.getName()))
+ {
+ return i;
+ }
+ }
+
+ return null;
+ }
+
+
+ @Override
+ protected int getRequiredPythonVersion()
+ {
+ return Python2Installation.PYTHON_VERSION;
+ }
+
+
+ public Object readResolve()
+ {
+ if (command != null)
+ {
+ scriptSource = new StringSource(command);
+ command = null;
+ }
+ return this;
+ }
+
+
+ @Initializer(before = PLUGINS_STARTED)
+ public static void addAliases()
+ {
+ File python3CompytibilityMarker = new File(Jenkins.getInstance().getRootDir(), "python3CompatibilityMarker");
+ if (!python3CompytibilityMarker.exists())
+ {
+ Items.XSTREAM2.addCompatibilityAlias("hudson.plugins.python.Python", Python2Script.class);
+ }
+
+ }
+
+ @Extension
+ public static final class DescriptorImpl extends AbstractPythonScriptDescriptor
+ {
+
+ public String getPythonVersion()
+ {
+ return "2";
+ }
+
+
+ public DescriptorImpl()
+ {
+ super(Python2Script.class);
+ load();
+ }
+
+
+ @Override
+ public String getDisplayName()
+ {
+ return "Execute Python 2 script";
+ }
+
+
+ @Override
+ public Python2Installation[] getInstallations()
+ {
+ return Jenkins.getInstance().getDescriptorByType(Python2Installation.Python2InstallationDescriptor.class)
+ .getInstallations();
+ }
+ }
+}
diff --git a/src/main/java/hudson/plugins/python/Python3Script.java b/src/main/java/hudson/plugins/python/Python3Script.java
new file mode 100644
index 0000000..3af05cd
--- /dev/null
+++ b/src/main/java/hudson/plugins/python/Python3Script.java
@@ -0,0 +1,129 @@
+/*
+ * The MIT License
+ *
+ * Copyright (c) 2016 Markus Winter. All rights reserved.
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in
+ * all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+ * THE SOFTWARE.
+ */
+package hudson.plugins.python;
+
+import static hudson.init.InitMilestone.PLUGINS_STARTED;
+
+import java.io.File;
+
+import org.kohsuke.stapler.DataBoundConstructor;
+
+import hudson.Extension;
+import hudson.init.Initializer;
+import hudson.model.Items;
+import hudson.plugins.python.installations.Python3Installation;
+import hudson.plugins.python.installations.Python3Installation.Python3InstallationDescriptor;
+import jenkins.model.Jenkins;
+import net.sf.json.JSONObject;
+
+public class Python3Script extends AbstractPythonScript
+{
+
+ private static final long serialVersionUID = -8017952220078123555L;
+
+ protected transient String command;
+
+ @DataBoundConstructor
+ public Python3Script(String pythonName, ScriptSource scriptSource, JSONObject python3Options, String scriptArgs,
+ String options, boolean quiet)
+ {
+ super(pythonName, scriptSource, scriptArgs, options, quiet);
+ }
+
+
+ @Override
+ protected Python3Installation getPython()
+ {
+ for (Python3Installation i : Jenkins.getInstance().getDescriptorByType(Python3InstallationDescriptor.class)
+ .getInstallations())
+ {
+ if (pythonName != null && pythonName.equals(i.getName()))
+ {
+ return i;
+ }
+ }
+
+ return null;
+ }
+
+
+ @Override
+ protected int getRequiredPythonVersion()
+ {
+ return Python3Installation.PYTHON_VERSION;
+ }
+
+ public Object readResolve()
+ {
+ if (command != null)
+ {
+ scriptSource = new StringSource(command);
+ command = null;
+ }
+ return this;
+ }
+
+
+ @Initializer(before = PLUGINS_STARTED)
+ public static void addAliases()
+ {
+ File python3CompytibilityMarker = new File(Jenkins.getInstance().getRootDir(), "python3CompatibilityMarker");
+ if (python3CompytibilityMarker.exists())
+ {
+ Items.XSTREAM2.addCompatibilityAlias("hudson.plugins.python.Python", Python3Script.class);
+ }
+ }
+
+ @Extension
+ public static final class DescriptorImpl extends AbstractPythonScriptDescriptor
+ {
+
+ public String getPythonVersion()
+ {
+ return "3";
+ }
+
+
+ public DescriptorImpl()
+ {
+ super(Python3Script.class);
+ load();
+ }
+
+
+ @Override
+ public String getDisplayName()
+ {
+ return "Execute Python 3 script";
+ }
+
+
+ @Override
+ public Python3Installation[] getInstallations()
+ {
+ return Jenkins.getInstance().getDescriptorByType(Python3Installation.Python3InstallationDescriptor.class)
+ .getInstallations();
+ }
+ }
+}
diff --git a/src/main/java/hudson/plugins/python/ScriptSource.java b/src/main/java/hudson/plugins/python/ScriptSource.java
new file mode 100644
index 0000000..5806c4e
--- /dev/null
+++ b/src/main/java/hudson/plugins/python/ScriptSource.java
@@ -0,0 +1,68 @@
+/*
+ * The MIT License
+ *
+ * Copyright (c) 2016 Markus Winter. All rights reserved.
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in
+ * all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+ * THE SOFTWARE.
+ */
+package hudson.plugins.python;
+
+import java.io.IOException;
+
+import hudson.DescriptorExtensionList;
+import hudson.FilePath;
+import hudson.model.AbstractBuild;
+import hudson.model.BuildListener;
+import hudson.model.Describable;
+import hudson.model.Descriptor;
+import jenkins.model.Jenkins;
+
+public abstract class ScriptSource implements Describable
+{
+
+ /**
+ * Able to load script when script path contains parameters
+ *
+ * @param projectWorkspace
+ * Project workspace to create tmp file
+ * @param build
+ * - needed to obtain environment variables
+ * @param listener
+ * - build listener needed by Environment
+ * @return Path to the executed script file
+ * @throws IOException
+ * @throws InterruptedException
+ */
+ public abstract FilePath getScriptFile(FilePath workspace, AbstractBuild, ?> build, BuildListener listener)
+ throws IOException, InterruptedException;
+
+
+ @Override
+ @SuppressWarnings("unchecked")
+ public Descriptor getDescriptor()
+ {
+ return Jenkins.getInstance().getDescriptorOrDie(getClass());
+ }
+
+
+ public static final DescriptorExtensionList> all()
+ {
+ return Jenkins.getInstance().getDescriptorList(ScriptSource.class);
+ }
+}
diff --git a/src/main/java/hudson/plugins/python/StringSource.java b/src/main/java/hudson/plugins/python/StringSource.java
new file mode 100644
index 0000000..b11c126
--- /dev/null
+++ b/src/main/java/hudson/plugins/python/StringSource.java
@@ -0,0 +1,85 @@
+/*
+ * The MIT License
+ *
+ * Copyright (c) 2016 Markus Winter. All rights reserved.
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in
+ * all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+ * THE SOFTWARE.
+ */
+package hudson.plugins.python;
+
+import java.io.IOException;
+
+import org.apache.commons.lang.StringUtils;
+import org.kohsuke.stapler.DataBoundConstructor;
+import org.kohsuke.stapler.QueryParameter;
+
+import hudson.Extension;
+import hudson.FilePath;
+import hudson.model.AbstractBuild;
+import hudson.model.BuildListener;
+import hudson.model.Descriptor;
+import hudson.util.FormValidation;
+
+public class StringSource extends ScriptSource
+{
+
+ private String scriptContent;
+
+
+ @DataBoundConstructor
+ public StringSource(String scriptContent)
+ {
+ this.scriptContent = scriptContent;
+ }
+
+
+ public String getScriptContent()
+ {
+ return scriptContent;
+ }
+
+
+ @Override
+ public FilePath getScriptFile(FilePath workspace, AbstractBuild, ?> build, BuildListener listener)
+ throws IOException, InterruptedException
+ {
+ return workspace.createTextTempFile("hudson", ".py", scriptContent, true);
+ }
+
+ @Extension
+ public static class DescriptorImpl extends Descriptor
+ {
+
+ @Override
+ public String getDisplayName()
+ {
+ return "Python script";
+ }
+
+
+ public FormValidation doCheckScript(@QueryParameter String scriptContent)
+ {
+ if (StringUtils.isBlank(scriptContent))
+ {
+ return FormValidation.error("Script seems to be empty string!");
+ }
+ return FormValidation.ok();
+ }
+ }
+}
diff --git a/src/main/java/hudson/plugins/python/installations/AbstractPythonInstallation.java b/src/main/java/hudson/plugins/python/installations/AbstractPythonInstallation.java
new file mode 100644
index 0000000..2165501
--- /dev/null
+++ b/src/main/java/hudson/plugins/python/installations/AbstractPythonInstallation.java
@@ -0,0 +1,68 @@
+/*
+ * The MIT License
+ *
+ * Copyright (c) 2016 Markus Winter. All rights reserved.
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in
+ * all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+ * THE SOFTWARE.
+ */
+package hudson.plugins.python.installations;
+
+import java.io.IOException;
+import java.util.List;
+
+import hudson.FilePath;
+import hudson.Launcher;
+import hudson.model.EnvironmentSpecific;
+import hudson.slaves.NodeSpecific;
+import hudson.tools.ToolInstallation;
+import hudson.tools.ToolProperty;
+
+public abstract class AbstractPythonInstallation extends ToolInstallation
+ implements EnvironmentSpecific, NodeSpecific
+{
+
+ private static final long serialVersionUID = 1L;
+
+
+ public AbstractPythonInstallation(String name, String home, List extends ToolProperty>> properties)
+ {
+ super(name, home, properties);
+ }
+
+
+ public PythonExecutable getExecutable(Launcher launcher) throws IOException, InterruptedException
+ {
+ PythonLocator locator = new PythonLocator(launcher);
+
+ FilePath pythonExe = new FilePath(launcher.getChannel(), getHome());
+ if (locator.getPythonMajorVersion(pythonExe, 0) == getRequiredPythonVersion())
+ {
+ if (pythonExe.getName().equals("py.exe"))
+ {
+ return new PythonExecutable(pythonExe, "-" + getRequiredPythonVersion());
+ }
+ return new PythonExecutable(pythonExe);
+ }
+ return null;
+
+ }
+
+
+ protected abstract int getRequiredPythonVersion();
+}
diff --git a/src/main/java/hudson/plugins/python/installations/Python2Installation.java b/src/main/java/hudson/plugins/python/installations/Python2Installation.java
new file mode 100644
index 0000000..316c915
--- /dev/null
+++ b/src/main/java/hudson/plugins/python/installations/Python2Installation.java
@@ -0,0 +1,117 @@
+/*
+ * The MIT License
+ *
+ * Copyright (c) 2016 Markus Winter. All rights reserved.
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in
+ * all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+ * THE SOFTWARE.
+ */
+package hudson.plugins.python.installations;
+
+import java.io.File;
+import java.io.IOException;
+import java.util.List;
+
+import org.kohsuke.stapler.DataBoundConstructor;
+import org.kohsuke.stapler.QueryParameter;
+import org.kohsuke.stapler.StaplerRequest;
+
+import hudson.EnvVars;
+import hudson.Extension;
+import hudson.model.Node;
+import hudson.model.TaskListener;
+import hudson.tools.ToolDescriptor;
+import hudson.tools.ToolProperty;
+import hudson.util.FormValidation;
+import jenkins.model.Jenkins;
+import net.sf.json.JSONObject;
+
+public class Python2Installation extends AbstractPythonInstallation
+{
+
+ private static final long serialVersionUID = 1386145821598783034L;
+
+ public static final transient String DEFAULT_PYTHON2 = "Default python 2";
+
+ public static final transient int PYTHON_VERSION = 2;
+
+
+ @DataBoundConstructor
+ public Python2Installation(String name, String home, List extends ToolProperty>> properties)
+ {
+ super(name, home, properties);
+ }
+
+
+ @Override
+ public Python2Installation forEnvironment(EnvVars environment)
+ {
+ return new Python2Installation(getName(), environment.expand(getHome()), getProperties().toList());
+ }
+
+
+ @Override
+ public Python2Installation forNode(Node node, TaskListener log) throws IOException, InterruptedException
+ {
+ return new Python2Installation(getName(), translateFor(node, log), getProperties().toList());
+ }
+
+
+ @Override
+ protected int getRequiredPythonVersion()
+ {
+ return 2;
+ }
+
+ @Extension
+ public static class Python2InstallationDescriptor extends ToolDescriptor
+ {
+
+ public Python2InstallationDescriptor()
+ {
+ super();
+ load();
+ }
+
+
+ @Override
+ public String getDisplayName()
+ {
+ return "Python 2";
+ }
+
+
+ @Override
+ public FormValidation doCheckHome(@QueryParameter File value)
+ {
+ Jenkins.getInstance().checkPermission(Jenkins.ADMINISTER);
+ String path = value.getPath();
+
+ return FormValidation.validateExecutable(path, new PythonValidator(PYTHON_VERSION));
+ }
+
+
+ @Override
+ public boolean configure(StaplerRequest req, JSONObject json) throws hudson.model.Descriptor.FormException
+ {
+ setInstallations(req.bindJSONToList(clazz, json.get("tool")).toArray(new Python2Installation[0]));
+ save();
+ return true;
+ }
+ }
+}
diff --git a/src/main/java/hudson/plugins/python/installations/Python3Installation.java b/src/main/java/hudson/plugins/python/installations/Python3Installation.java
new file mode 100644
index 0000000..f39b07b
--- /dev/null
+++ b/src/main/java/hudson/plugins/python/installations/Python3Installation.java
@@ -0,0 +1,116 @@
+/*
+ * The MIT License
+ *
+ * Copyright (c) 2016 Markus Winter. All rights reserved.
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in
+ * all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+ * THE SOFTWARE.
+ */
+package hudson.plugins.python.installations;
+
+import java.io.File;
+import java.io.IOException;
+import java.util.List;
+
+import org.kohsuke.stapler.DataBoundConstructor;
+import org.kohsuke.stapler.QueryParameter;
+import org.kohsuke.stapler.StaplerRequest;
+
+import hudson.EnvVars;
+import hudson.Extension;
+import hudson.model.Node;
+import hudson.model.TaskListener;
+import hudson.tools.ToolDescriptor;
+import hudson.tools.ToolProperty;
+import hudson.util.FormValidation;
+import jenkins.model.Jenkins;
+import net.sf.json.JSONObject;
+
+public class Python3Installation extends AbstractPythonInstallation
+{
+
+ private static final long serialVersionUID = -5394519702437683194L;
+
+ public static final transient String DEFAULT_PYTHON3 = "Default python 3";
+ public static final transient int PYTHON_VERSION = 3;
+
+
+ @DataBoundConstructor
+ public Python3Installation(String name, String home, List extends ToolProperty>> properties)
+ {
+ super(name, home, properties);
+ }
+
+
+ @Override
+ public Python3Installation forEnvironment(EnvVars environment)
+ {
+ return new Python3Installation(getName(), environment.expand(getHome()), getProperties().toList());
+ }
+
+
+ @Override
+ public Python3Installation forNode(Node node, TaskListener log) throws IOException, InterruptedException
+ {
+ return new Python3Installation(getName(), translateFor(node, log), getProperties().toList());
+ }
+
+
+ @Override
+ protected int getRequiredPythonVersion()
+ {
+ return PYTHON_VERSION;
+ }
+
+ @Extension
+ public static class Python3InstallationDescriptor extends ToolDescriptor
+ {
+
+ public Python3InstallationDescriptor()
+ {
+ super();
+ load();
+ }
+
+
+ @Override
+ public String getDisplayName()
+ {
+ return "Python 3";
+ }
+
+
+ @Override
+ public FormValidation doCheckHome(@QueryParameter File value)
+ {
+ Jenkins.getInstance().checkPermission(Jenkins.ADMINISTER);
+ String path = value.getPath();
+
+ return FormValidation.validateExecutable(path, new PythonValidator(PYTHON_VERSION));
+ }
+
+
+ @Override
+ public boolean configure(StaplerRequest req, JSONObject json) throws hudson.model.Descriptor.FormException
+ {
+ setInstallations(req.bindJSONToList(clazz, json.get("tool")).toArray(new Python3Installation[0]));
+ save();
+ return true;
+ }
+ }
+}
diff --git a/src/main/java/hudson/plugins/python/installations/PythonExecutable.java b/src/main/java/hudson/plugins/python/installations/PythonExecutable.java
new file mode 100644
index 0000000..36d18e0
--- /dev/null
+++ b/src/main/java/hudson/plugins/python/installations/PythonExecutable.java
@@ -0,0 +1,60 @@
+/*
+ * The MIT License
+ *
+ * Copyright (c) 2016 Markus Winter. All rights reserved.
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in
+ * all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+ * THE SOFTWARE.
+ */
+package hudson.plugins.python.installations;
+
+import java.util.ArrayList;
+import java.util.List;
+
+import hudson.FilePath;
+
+public class PythonExecutable
+{
+ private FilePath executable;
+ private List arguments = new ArrayList();
+
+
+ public PythonExecutable(FilePath executable, String... arguments)
+ {
+ this.executable = executable;
+ for (String arg : arguments)
+ {
+ if (arg != null && arg.length() > 0)
+ {
+ this.arguments.add(arg);
+ }
+ }
+ }
+
+
+ public String getExecutable()
+ {
+ return executable.getRemote();
+ }
+
+
+ public List getArguments()
+ {
+ return arguments;
+ }
+}
diff --git a/src/main/java/hudson/plugins/python/installations/PythonLocator.java b/src/main/java/hudson/plugins/python/installations/PythonLocator.java
new file mode 100644
index 0000000..540d6f6
--- /dev/null
+++ b/src/main/java/hudson/plugins/python/installations/PythonLocator.java
@@ -0,0 +1,224 @@
+/*
+ * The MIT License
+ *
+ * Copyright (c) 2016 Markus Winter. All rights reserved.
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in
+ * all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+ * THE SOFTWARE.
+ */
+package hudson.plugins.python.installations;
+
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
+import java.util.logging.Logger;
+
+import javax.annotation.CheckForNull;
+
+import hudson.EnvVars;
+import hudson.FilePath;
+import hudson.Launcher;
+import hudson.Util;
+import hudson.remoting.VirtualChannel;
+import hudson.util.ArgumentListBuilder;
+
+public class PythonLocator
+{
+
+ private Launcher launcher;
+ private VirtualChannel channel;
+
+
+ public PythonLocator(Launcher launcher)
+ {
+ this.launcher = launcher;
+ channel = launcher.getChannel();
+ }
+
+ private static Logger logger = Logger.getLogger(PythonLocator.class.getName());
+
+
+ @CheckForNull
+ public PythonExecutable findPythonForVersion(int version)
+ throws IOException, InterruptedException
+ {
+ if (launcher.isUnix())
+ {
+ return findPythonVersionForUnix(version);
+ }
+ else
+ {
+ return findPythonVersionForWindows(version);
+ }
+ }
+
+
+ /*
+ * Looks in the PATH for a given file
+ */
+ @CheckForNull
+ public FilePath findFileInPath(String exe) throws IOException, InterruptedException
+ {
+ EnvVars envvars = EnvVars.getRemote(channel);
+ String path = envvars.get("PATH");
+ logger.fine("PATH is : " + path);
+ if (path != null)
+ {
+ for (String dir : Util.tokenize(path.replace("\\", "\\\\"), launcher.isUnix() ? ":" : ";"))
+ {
+ logger.fine("Scanning directory " + dir);
+ FilePath dirPath = new FilePath(channel, dir);
+
+ FilePath f = dirPath.child(exe);
+ logger.fine("Looking for file " + f.getRemote());
+ if (f.exists())
+ {
+ return f;
+ }
+ }
+ }
+
+ return null;
+ }
+
+
+ @CheckForNull
+ public PythonExecutable findPythonVersionForWindows(int wantedVersion) throws IOException, InterruptedException
+ {
+ FilePath pythonExe = findFileInPath("py.exe");
+ logger.fine("PythonExe is " + pythonExe);
+ int version;
+ if (pythonExe != null)
+ {
+ version = getPythonMajorVersion(pythonExe, wantedVersion);
+ if (version == wantedVersion)
+ {
+ return new PythonExecutable(pythonExe, "-" + wantedVersion);
+ }
+ }
+
+ pythonExe = findFileInPath("python.exe");
+ logger.fine("PythonExe is " + pythonExe);
+ version = 0;
+ if (pythonExe != null)
+ {
+ version = getPythonMajorVersion(pythonExe, 0);
+ if (version == wantedVersion)
+ {
+ return new PythonExecutable(pythonExe);
+ }
+ }
+
+ FilePath rootC = new FilePath(channel, "c:\\");
+ for (FilePath dir : rootC.listDirectories())
+ {
+ logger.fine("searching for python in " + dir.getName());
+ if (dir.getName().toLowerCase().startsWith("python" + wantedVersion))
+ {
+ logger.fine("Directory starts with python " + dir);
+ pythonExe = dir.child("python.exe");
+ if (!pythonExe.isDirectory())
+ {
+ logger.fine("PythonExe exists " + pythonExe);
+ version = getPythonMajorVersion(pythonExe, 0);
+ if (version == wantedVersion)
+ {
+ return new PythonExecutable(pythonExe);
+ }
+ }
+ }
+ }
+ return null;
+ }
+
+
+ @CheckForNull
+ public PythonExecutable findPythonVersionForUnix(int wantedVersion) throws IOException, InterruptedException
+ {
+ logger.fine("Searching for python" + wantedVersion);
+
+ FilePath pythonExe = findFileInPath("python" + wantedVersion);
+ int version;
+ if (pythonExe != null)
+ {
+ logger.fine("Found: " + pythonExe);
+ version = getPythonMajorVersion(pythonExe, 0);
+ if (version == wantedVersion)
+ {
+ logger.fine("Found python for Unix: " + pythonExe.getRemote());
+ return new PythonExecutable(pythonExe);
+ }
+ }
+
+ logger.fine("Searching for python");
+ pythonExe = findFileInPath("python");
+ if (pythonExe != null)
+ {
+ version = getPythonMajorVersion(pythonExe, 0);
+ if (version == wantedVersion)
+ {
+ logger.fine("Found python: " + pythonExe.getRemote());
+ return new PythonExecutable(pythonExe);
+ }
+ }
+
+ return null;
+ }
+
+
+ /**
+ * Starts python and retrieves the python major version. Return 0 if the file is not python or some other
+ * error
+ * occured.
+ *
+ * @param file
+ * @param python3Option
+ * @return
+ * @throws IOException
+ * @throws InterruptedException
+ */
+ public int getPythonMajorVersion(FilePath file, int wantedVersion) throws InterruptedException, IOException
+ {
+ ByteArrayOutputStream output = new ByteArrayOutputStream();
+ ArgumentListBuilder command = new ArgumentListBuilder();
+ command.add(file.getRemote());
+ if (wantedVersion > 0)
+ {
+ command.addTokenized("-" + wantedVersion);
+ }
+ command.add("-c").add("import sys; print(sys.version_info[0])");
+ logger.fine("Checking python Version of " + file.getRemote());
+ logger.fine("Command: " + command.toString());
+
+ final Launcher.ProcStarter proc = launcher.launch().cmds(command).stdout(output).quiet(true);
+
+ if (proc.join() == 0)
+ {
+ try
+ {
+ logger.fine("Python Version is " + output.toString().trim());
+ return Integer.parseInt(output.toString().trim());
+ }
+ catch (NumberFormatException e)
+ {
+ return 0;
+ }
+ }
+
+ return 0;
+ }
+}
diff --git a/src/main/java/hudson/plugins/python/installations/PythonValidator.java b/src/main/java/hudson/plugins/python/installations/PythonValidator.java
new file mode 100644
index 0000000..8a84449
--- /dev/null
+++ b/src/main/java/hudson/plugins/python/installations/PythonValidator.java
@@ -0,0 +1,72 @@
+/*
+ * The MIT License
+ *
+ * Copyright (c) 2016 Markus Winter. All rights reserved.
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in
+ * all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+ * THE SOFTWARE.
+ */
+package hudson.plugins.python.installations;
+
+import java.io.File;
+import java.io.IOException;
+
+import hudson.FilePath;
+import hudson.Launcher;
+import hudson.model.TaskListener;
+import hudson.util.FormValidation;
+import hudson.util.FormValidation.FileValidator;
+import jenkins.model.Jenkins;
+
+public class PythonValidator extends FileValidator
+{
+ private final int version;
+
+
+ public PythonValidator(int version)
+ {
+ this.version = version;
+ }
+
+
+ @Override
+ public FormValidation validate(File f)
+ {
+ try
+ {
+ Launcher launcher = Jenkins.getInstance().createLauncher(TaskListener.NULL);
+ PythonLocator locator = new PythonLocator(launcher);
+ int effectiveVersion = locator.getPythonMajorVersion(new FilePath(f), 0);
+ if (effectiveVersion != version)
+ {
+ return FormValidation.error(
+ "The python found at " + f.getPath() + " has major version " + effectiveVersion
+ + " but expected major version is " + version + ".");
+ }
+ return FormValidation.ok();
+ }
+ catch (IOException e)
+ {
+ return FormValidation.error("Unable to check python version.");
+ }
+ catch (InterruptedException e)
+ {
+ return FormValidation.error("Unable to check python version.");
+ }
+ }
+}
diff --git a/src/main/resources/hudson/plugins/python/FileScriptSource/config.jelly b/src/main/resources/hudson/plugins/python/FileScriptSource/config.jelly
new file mode 100644
index 0000000..d4d6a85
--- /dev/null
+++ b/src/main/resources/hudson/plugins/python/FileScriptSource/config.jelly
@@ -0,0 +1,30 @@
+
+
+
+
+
+
\ No newline at end of file
diff --git a/src/main/resources/hudson/plugins/python/FileScriptSource/help-scriptFile.html b/src/main/resources/hudson/plugins/python/FileScriptSource/help-scriptFile.html
new file mode 100644
index 0000000..038a2d8
--- /dev/null
+++ b/src/main/resources/hudson/plugins/python/FileScriptSource/help-scriptFile.html
@@ -0,0 +1,3 @@
+
+A file with a python script. If a relative path is given, it will be resolved against the jobs workspace.
+
\ No newline at end of file
diff --git a/src/main/resources/hudson/plugins/python/Messages.properties b/src/main/resources/hudson/plugins/python/Messages.properties
deleted file mode 100644
index 81c187f..0000000
--- a/src/main/resources/hudson/plugins/python/Messages.properties
+++ /dev/null
@@ -1 +0,0 @@
-Python.DisplayName=Execute Python script
\ No newline at end of file
diff --git a/src/main/resources/hudson/plugins/python/Python/config.jelly b/src/main/resources/hudson/plugins/python/Python/config.jelly
deleted file mode 100644
index a28fac7..0000000
--- a/src/main/resources/hudson/plugins/python/Python/config.jelly
+++ /dev/null
@@ -1,8 +0,0 @@
-
-
-
-
-
-
diff --git a/src/main/resources/hudson/plugins/python/Python/config.properties b/src/main/resources/hudson/plugins/python/Python/config.properties
deleted file mode 100644
index 391b867..0000000
--- a/src/main/resources/hudson/plugins/python/Python/config.properties
+++ /dev/null
@@ -1 +0,0 @@
-blurb=See the list of available environment variables
\ No newline at end of file
diff --git a/src/main/resources/hudson/plugins/python/Python/help.html b/src/main/resources/hudson/plugins/python/Python/help.html
deleted file mode 100644
index 49cc7fd..0000000
--- a/src/main/resources/hudson/plugins/python/Python/help.html
+++ /dev/null
@@ -1,6 +0,0 @@
-
-
- Runs a Python script (defaults to python interpreter) for building the project.
- The script will be run with the workspace as the current directory.
-
+Specify the python options that should be set in the call. Not all options are available in every version of python2,
+so verify that all of the options will work on all of your connected agents.
+See the Python 2 command line documentation for a list of valid options.
+Take care not to use any options that will enter interactive mode, i.e. "-i"
+
Use the EnvInject Plugin to set environment variables that can be used as well to set certain options.
+
diff --git a/src/main/resources/hudson/plugins/python/Python3Script/config.jelly b/src/main/resources/hudson/plugins/python/Python3Script/config.jelly
new file mode 100644
index 0000000..89b09d6
--- /dev/null
+++ b/src/main/resources/hudson/plugins/python/Python3Script/config.jelly
@@ -0,0 +1,57 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/src/main/resources/hudson/plugins/python/Python3Script/help-options.html b/src/main/resources/hudson/plugins/python/Python3Script/help-options.html
new file mode 100644
index 0000000..6f42898
--- /dev/null
+++ b/src/main/resources/hudson/plugins/python/Python3Script/help-options.html
@@ -0,0 +1,8 @@
+
+Specify the python options that should be set in the call. Not all options are available in every version of python2,
+so verify that all of the options will work on all of your connected agents.
+See the Python 3 command line documentation for a list of valid options.
+Take care not to use any options that will enter interactive mode, i.e. "-i"
+
+Use the EnvInject Plugin to set environment variables that can be used as well to set certain options.
+
diff --git a/src/main/resources/hudson/plugins/python/StringSource/config.jelly b/src/main/resources/hudson/plugins/python/StringSource/config.jelly
new file mode 100644
index 0000000..49033d7
--- /dev/null
+++ b/src/main/resources/hudson/plugins/python/StringSource/config.jelly
@@ -0,0 +1,34 @@
+
+
+
+
+
+
+
+
+
diff --git a/src/main/resources/hudson/plugins/python/form/multi-hetero-radio.jelly b/src/main/resources/hudson/plugins/python/form/multi-hetero-radio.jelly
new file mode 100644
index 0000000..9f609ac
--- /dev/null
+++ b/src/main/resources/hudson/plugins/python/form/multi-hetero-radio.jelly
@@ -0,0 +1,68 @@
+
+
+
+
+
+ Sibling of hetero-list, which only allows the user to pick one type from the list of descriptors and configure
+ it.
+
+
+ Field name in the parent object where databinding happens.
+
+
+ all types that the user can add.
+
+
+ Id of the radioBlock group.
+
+
+ Default option to be used.
+
+
+ The Python Script descriptor
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/main/resources/hudson/plugins/python/form/taglib b/src/main/resources/hudson/plugins/python/form/taglib
new file mode 100644
index 0000000..e69de29
diff --git a/src/main/resources/hudson/plugins/python/installations/Python2Installation/config.jelly b/src/main/resources/hudson/plugins/python/installations/Python2Installation/config.jelly
new file mode 100644
index 0000000..933475a
--- /dev/null
+++ b/src/main/resources/hudson/plugins/python/installations/Python2Installation/config.jelly
@@ -0,0 +1,33 @@
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/src/main/resources/hudson/plugins/python/installations/Python3Installation/config.jelly b/src/main/resources/hudson/plugins/python/installations/Python3Installation/config.jelly
new file mode 100644
index 0000000..d6fc466
--- /dev/null
+++ b/src/main/resources/hudson/plugins/python/installations/Python3Installation/config.jelly
@@ -0,0 +1,33 @@
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/src/main/webapp/help/python-version.html b/src/main/webapp/help/python-version.html
new file mode 100644
index 0000000..857da79
--- /dev/null
+++ b/src/main/webapp/help/python-version.html
@@ -0,0 +1,14 @@
+
+ Python installation which will execute the script. Specify the name of the Python installation as specified in the Global Jenkins configuration.
+ By using the default configuration, the plugin will try to find the corresponding Python version and use it.
+
+ Windows
+ First it is checked if the Python launcher is available in the path (PEP-397) and if it is possible to start the desired Python.
+ If the Python launcher is not available, the plugin will check if there is a python.exe in the path for the desired version. Finally it will search a python.exe in sub folders
+ of the c:-drive who's name starts with "python" followed by the desired version, e.g. c:\python27.
+
+
+ Unix
+ The plugin will look for the corresponding python2 or python3 in the path, if not found it will check if python is available and provides the correct version.
+
+
\ No newline at end of file
diff --git a/src/main/webapp/help/script-arguments.html b/src/main/webapp/help/script-arguments.html
new file mode 100644
index 0000000..0ddf9e6
--- /dev/null
+++ b/src/main/webapp/help/script-arguments.html
@@ -0,0 +1,3 @@
+
+ The arguments that will be passed to the script.
+
\ No newline at end of file
diff --git a/src/test/java/hudson/plugins/python/PythonTest.java b/src/test/java/hudson/plugins/python/PythonTest.java
deleted file mode 100644
index eb6cfda..0000000
--- a/src/test/java/hudson/plugins/python/PythonTest.java
+++ /dev/null
@@ -1,25 +0,0 @@
-package hudson.plugins.python;
-
-import hudson.model.FreeStyleProject;
-import org.jvnet.hudson.test.HudsonTestCase;
-
-/**
- * @author Kohsuke Kawaguchi
- */
-public class PythonTest extends HudsonTestCase {
- public void testConfigRoundtrip() throws Exception {
-
- FreeStyleProject p = createFreeStyleProject();
- Python before = new Python("print 5");
- p.getBuildersList().add(before);
-
- configRoundtrip(p);
-
- Python after = p.getBuildersList().get(Python.class);
-
- assertNotSame(before,after);
- assertEqualDataBoundBeans(before,after);
-
- assertBuildStatusSuccess(p.scheduleBuild2(0));
- }
-}
diff --git a/src/test/java/hudson/plugins/python/installations/PythonInstallationTest.java b/src/test/java/hudson/plugins/python/installations/PythonInstallationTest.java
new file mode 100644
index 0000000..4f41d81
--- /dev/null
+++ b/src/test/java/hudson/plugins/python/installations/PythonInstallationTest.java
@@ -0,0 +1,171 @@
+/*
+ * The MIT License
+ *
+ * Copyright (c) 2016 Markus Winter. All rights reserved.
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in
+ * all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+ * THE SOFTWARE.
+ */
+package hudson.plugins.python.installations;
+
+import static org.easymock.EasyMock.anyObject;
+import static org.easymock.EasyMock.expect;
+import static org.hamcrest.Matchers.contains;
+import static org.hamcrest.Matchers.empty;
+import static org.hamcrest.Matchers.is;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertThat;
+import static org.powermock.api.easymock.PowerMock.expectNew;
+
+import java.io.ByteArrayOutputStream;
+import java.io.File;
+
+import org.apache.commons.io.FileUtils;
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.rules.TemporaryFolder;
+import org.junit.runner.RunWith;
+import org.powermock.api.easymock.PowerMock;
+import org.powermock.core.classloader.annotations.PrepareForTest;
+import org.powermock.modules.junit4.PowerMockRunner;
+
+import hudson.Launcher;
+import hudson.remoting.LocalChannel;
+import hudson.remoting.VirtualChannel;
+import hudson.util.ArgumentListBuilder;
+
+@RunWith(PowerMockRunner.class)
+@PrepareForTest({ PythonLocator.class, Launcher.ProcStarter.class, Launcher.class })
+
+public class PythonInstallationTest
+{
+ @Rule
+ private TemporaryFolder tmpRule = new TemporaryFolder();
+
+ File python2Folder;
+ File python3Folder;
+ File python2;
+ File python3;
+ File python3ux;
+ File python3win;
+
+ Launcher launcher;
+ Launcher.ProcStarter proc;
+ VirtualChannel channel;
+
+
+ @Before
+ public void setup() throws Exception
+ {
+ python2Folder = tmpRule.newFolder("Python2");
+ python3Folder = tmpRule.newFolder("Python3");
+ python2 = new File(python2Folder, "py.exe");
+ python3 = new File(python3Folder, "py.exe");
+ python3win = new File(python3Folder, "python.exe");
+ python3ux = new File(python3Folder, "python3");
+ FileUtils.writeStringToFile(python2, "");
+ FileUtils.writeStringToFile(python3, "");
+ FileUtils.writeStringToFile(python3ux, "");
+ launcher = PowerMock.createMock(Launcher.class);
+ proc = PowerMock.createMock(Launcher.ProcStarter.class);
+ channel = PowerMock.createMock(LocalChannel.class);
+ expect(launcher.getChannel()).andReturn(channel);
+ }
+
+
+ @Test
+ public void testPython2InstallationReturns2AsRequired() throws Exception
+ {
+ Python2Installation python2Installation = new Python2Installation("python2", python2.getAbsolutePath(), null);
+ assertEquals(python2Installation.getRequiredPythonVersion(), 2);
+ }
+
+
+ private void defineExpectsForGetPythonMajorVersion(char version) throws Exception
+ {
+ expect(launcher.launch()).andReturn(proc);
+ expect(proc.cmds(anyObject(ArgumentListBuilder.class))).andReturn(proc);
+ expect(proc.stdout(anyObject(ByteArrayOutputStream.class))).andReturn(proc);
+ expect(proc.quiet(true)).andReturn(proc);
+ expect(proc.join()).andReturn(0);
+ ByteArrayOutputStream output = new ByteArrayOutputStream();
+ output.write(version);
+
+ expectNew(ByteArrayOutputStream.class).andReturn(output);
+ }
+
+
+ @Test
+ public void testPython2InstallationViaPy() throws Exception
+ {
+ defineExpectsForGetPythonMajorVersion('2');
+ expect(launcher.getChannel()).andReturn(channel);
+
+ PowerMock.replayAll();
+
+ Python2Installation python2Installation = new Python2Installation("python2", python2.getAbsolutePath(), null);
+ PythonExecutable executable = python2Installation.getExecutable(launcher);
+ assertThat(executable.getArguments(), contains("-2"));
+ }
+
+
+ @Test
+ public void testPython3InstallationViaPy() throws Exception
+ {
+ defineExpectsForGetPythonMajorVersion('3');
+ expect(launcher.getChannel()).andReturn(channel);
+
+ PowerMock.replayAll();
+
+ Python3Installation python2Installation = new Python3Installation("python3", python3.getAbsolutePath(), null);
+ PythonExecutable executable = python2Installation.getExecutable(launcher);
+ assertThat(executable.getArguments(), contains("-3"));
+ }
+
+
+ @Test
+ public void testPython3InstallationUnix() throws Exception
+ {
+ defineExpectsForGetPythonMajorVersion('3');
+ expect(launcher.getChannel()).andReturn(channel);
+
+ PowerMock.replayAll();
+
+ Python3Installation python3Installation = new Python3Installation("python3", python3ux.getAbsolutePath(), null);
+ PythonExecutable executable = python3Installation.getExecutable(launcher);
+ assertThat(executable.getArguments(), empty());
+ assertThat(executable.getExecutable(), is(python3ux.getAbsolutePath()));
+ }
+
+
+ @Test
+ public void testPython3InstallationWindows() throws Exception
+ {
+ defineExpectsForGetPythonMajorVersion('3');
+ expect(launcher.getChannel()).andReturn(channel);
+
+ PowerMock.replayAll();
+
+ Python3Installation python3Installation = new Python3Installation("python3", python3win.getAbsolutePath(),
+ null);
+ PythonExecutable executable = python3Installation.getExecutable(launcher);
+ assertThat(executable.getArguments(), empty());
+ assertThat(executable.getExecutable(), is(python3win.getAbsolutePath()));
+ }
+}
diff --git a/src/test/java/hudson/plugins/python/installations/PythonLocatorTest.java b/src/test/java/hudson/plugins/python/installations/PythonLocatorTest.java
new file mode 100644
index 0000000..b10428b
--- /dev/null
+++ b/src/test/java/hudson/plugins/python/installations/PythonLocatorTest.java
@@ -0,0 +1,303 @@
+/*
+ * The MIT License
+ *
+ * Copyright (c) 2016 Markus Winter. All rights reserved.
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in
+ * all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+ * THE SOFTWARE.
+ */
+package hudson.plugins.python.installations;
+
+import static org.easymock.EasyMock.anyObject;
+import static org.easymock.EasyMock.expect;
+import static org.hamcrest.Matchers.is;
+import static org.hamcrest.Matchers.notNullValue;
+import static org.junit.Assert.assertThat;
+import static org.powermock.api.easymock.PowerMock.expectNew;
+
+import java.io.ByteArrayOutputStream;
+import java.io.File;
+import java.io.IOException;
+
+import org.apache.commons.io.FileUtils;
+import org.easymock.EasyMockSupport;
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.rules.TemporaryFolder;
+import org.junit.runner.RunWith;
+import org.powermock.api.easymock.PowerMock;
+import org.powermock.core.classloader.annotations.PrepareForTest;
+import org.powermock.modules.junit4.PowerMockRunner;
+
+import hudson.EnvVars;
+import hudson.FilePath;
+import hudson.Launcher;
+import hudson.remoting.LocalChannel;
+import hudson.remoting.VirtualChannel;
+import hudson.util.ArgumentListBuilder;
+
+@RunWith(PowerMockRunner.class)
+@PrepareForTest({ VirtualChannel.class, PythonLocator.class, Launcher.class, Launcher.ProcStarter.class,
+ EnvVars.class })
+public class PythonLocatorTest extends EasyMockSupport
+{
+
+ @Rule
+ private TemporaryFolder tmpRule = new TemporaryFolder();
+
+ FilePath tmpPython;
+ File windows;
+ File python2Folder;
+ File python3Folder;
+ Launcher launcher;
+ Launcher.ProcStarter proc;
+ VirtualChannel channel;
+ File py;
+ File python2;
+ File python3;
+ File python2ux;
+ File python3ux;
+
+
+ @Before
+ public void setup() throws Exception
+ {
+
+ python2Folder = tmpRule.newFolder("Python2");
+ python3Folder = tmpRule.newFolder("Python3");
+ windows = tmpRule.newFolder("Windows");
+ File tmpFile = tmpRule.newFile("python.exe");
+ tmpPython = new FilePath(tmpFile);
+ python2 = new File(python2Folder, "python.exe");
+ python3 = new File(python3Folder, "python.exe");
+ python2ux = new File(python2Folder, "python2");
+ python3ux = new File(python3Folder, "python3");
+ py = new File(windows, "py.exe");
+ FileUtils.writeStringToFile(python2, "");
+ FileUtils.writeStringToFile(python3, "");
+ FileUtils.writeStringToFile(python2ux, "");
+ FileUtils.writeStringToFile(python3ux, "");
+ FileUtils.writeStringToFile(py, "");
+ FileUtils.writeStringToFile(tmpFile, "");
+
+ launcher = PowerMock.createMock(Launcher.class);
+ proc = PowerMock.createMock(Launcher.ProcStarter.class);
+ channel = PowerMock.createMock(LocalChannel.class);
+ expect(launcher.getChannel()).andReturn(channel);
+ }
+
+
+ private void defineExpectsForGetPythonMajorVersion(char version) throws Exception
+ {
+ expect(launcher.launch()).andReturn(proc);
+ expect(proc.cmds(anyObject(ArgumentListBuilder.class))).andReturn(proc);
+ expect(proc.stdout(anyObject(ByteArrayOutputStream.class))).andReturn(proc);
+ expect(proc.quiet(true)).andReturn(proc);
+ expect(proc.join()).andReturn(0);
+ ByteArrayOutputStream output = new ByteArrayOutputStream();
+ output.write(version);
+
+ expectNew(ByteArrayOutputStream.class).andReturn(output);
+ }
+
+
+ @Test
+ public void testFileInPathWithExeExtensionIsFound() throws IOException, InterruptedException
+ {
+ EnvVars envvars = PowerMock.createMock(EnvVars.class);
+ PowerMock.mockStatic(EnvVars.class);
+ defineExpectsForFindFileInPath(python2Folder, envvars, false);
+
+ PowerMock.replayAll();
+
+ PythonLocator locator = new PythonLocator(launcher);
+ FilePath python = locator.findFileInPath("python.exe");
+ assertThat(python, is(notNullValue()));
+ }
+
+
+ @Test
+ public void testGetPython2Version() throws Exception
+ {
+
+ defineExpectsForGetPythonMajorVersion('2');
+ PowerMock.replayAll();
+
+ PythonLocator locator = new PythonLocator(launcher);
+ int version = locator.getPythonMajorVersion(tmpPython, Python2Installation.PYTHON_VERSION);
+ assertThat(version, is(Python2Installation.PYTHON_VERSION));
+ PowerMock.verifyAll();
+ }
+
+
+ @Test
+ public void testGetPython3Version() throws Exception
+ {
+ defineExpectsForGetPythonMajorVersion('3');
+
+ PowerMock.replayAll();
+
+ PythonLocator locator = new PythonLocator(launcher);
+ int version = locator.getPythonMajorVersion(tmpPython, Python3Installation.PYTHON_VERSION);
+ assertThat(version, is(Python3Installation.PYTHON_VERSION));
+ PowerMock.verifyAll();
+ }
+
+
+ private String unixLikePath(String path)
+ {
+ path = path.replaceAll("\\\\", "/");
+ path = path.replaceFirst("^.:", "");
+ return path;
+ }
+
+
+ private void defineExpectsForFindFileInPath(File folder, EnvVars envvars, boolean isUnix)
+ throws IOException, InterruptedException
+ {
+ expect(EnvVars.getRemote(channel)).andReturn(envvars);
+ String path = folder.getPath();
+ if (isUnix)
+ {
+ path = unixLikePath(path);
+ }
+
+ expect(envvars.get("PATH")).andReturn(path);
+ expect(launcher.isUnix()).andReturn(isUnix);
+ }
+
+
+ @Test
+ public void testFindPython2ForWindowsViaPy() throws Exception
+ {
+ EnvVars envvars = PowerMock.createMock(EnvVars.class);
+ PowerMock.mockStatic(EnvVars.class);
+ defineExpectsForFindFileInPath(windows, envvars, false);
+ defineExpectsForGetPythonMajorVersion('2');
+
+ PowerMock.replayAll();
+
+ PythonLocator locator = new PythonLocator(launcher);
+
+ PythonExecutable f = locator.findPythonVersionForWindows(2);
+ assertThat(f.getExecutable(), is(py.getAbsolutePath()));
+ PowerMock.verifyAll();
+ }
+
+
+ @Test
+ public void testFindPython2ForWindowsViaPython() throws Exception
+ {
+ EnvVars envvars = PowerMock.createMock(EnvVars.class);
+ PowerMock.mockStatic(EnvVars.class);
+ defineExpectsForFindFileInPath(python2Folder, envvars, false);
+ defineExpectsForFindFileInPath(python2Folder, envvars, false);
+ defineExpectsForGetPythonMajorVersion('2');
+
+ PowerMock.replayAll();
+
+ PythonLocator locator = new PythonLocator(launcher);
+
+ PythonExecutable f = locator.findPythonVersionForWindows(2);
+ assertThat(f.getExecutable().toLowerCase(), is(python2.getAbsolutePath().toLowerCase()));
+ PowerMock.verifyAll();
+ }
+
+
+ @Test
+ public void testFindPython3ForWindowsViaPy() throws Exception
+ {
+ EnvVars envvars = PowerMock.createMock(EnvVars.class);
+ PowerMock.mockStatic(EnvVars.class);
+ defineExpectsForFindFileInPath(windows, envvars, false);
+ defineExpectsForGetPythonMajorVersion('3');
+
+ PowerMock.replayAll();
+
+ PythonLocator locator = new PythonLocator(launcher);
+
+ PythonExecutable f = locator.findPythonVersionForWindows(Python3Installation.PYTHON_VERSION);
+ assertThat(f.getExecutable(), is(py.getAbsolutePath()));
+ PowerMock.verifyAll();
+
+ }
+
+
+ @Test
+ public void testFindPython3ForWindowsViaPython() throws Exception
+ {
+ EnvVars envvars = PowerMock.createMock(EnvVars.class);
+ PowerMock.mockStatic(EnvVars.class);
+ defineExpectsForFindFileInPath(python3Folder, envvars, false);
+ defineExpectsForFindFileInPath(python3Folder, envvars, false);
+ defineExpectsForGetPythonMajorVersion('3');
+
+ PowerMock.replayAll();
+
+ PythonLocator locator = new PythonLocator(launcher);
+
+ PythonExecutable f = locator.findPythonVersionForWindows(Python3Installation.PYTHON_VERSION);
+ assertThat(f.getExecutable().toLowerCase(), is(python3.getAbsolutePath().toLowerCase()));
+ PowerMock.verifyAll();
+ }
+
+
+ // This test runs on windows only when the tmp folder is on the same drive
+ // where the test is executed
+ @Test
+ public void testFindPython2ForUnix() throws Exception
+ {
+ EnvVars envvars = PowerMock.createMock(EnvVars.class);
+ PowerMock.mockStatic(EnvVars.class);
+ defineExpectsForFindFileInPath(python2Folder, envvars, true);
+ defineExpectsForGetPythonMajorVersion('2');
+
+ PowerMock.replayAll();
+
+ PythonLocator locator = new PythonLocator(launcher);
+
+ PythonExecutable f = locator.findPythonVersionForUnix(Python2Installation.PYTHON_VERSION);
+ String expectedPath = unixLikePath(python2ux.getAbsolutePath().toLowerCase());
+ String foundPath = unixLikePath(f.getExecutable().toLowerCase());
+ assertThat(foundPath, is(expectedPath));
+ PowerMock.verifyAll();
+ }
+
+
+ // This test runs on windows only when the tmp folder is on the same drive
+ // where the test is executed
+ @Test
+ public void testFindPython3ForUnix() throws Exception
+ {
+ EnvVars envvars = PowerMock.createMock(EnvVars.class);
+ PowerMock.mockStatic(EnvVars.class);
+ defineExpectsForFindFileInPath(python3Folder, envvars, true);
+ defineExpectsForGetPythonMajorVersion('3');
+
+ PowerMock.replayAll();
+
+ PythonLocator locator = new PythonLocator(launcher);
+
+ PythonExecutable f = locator.findPythonVersionForUnix(Python3Installation.PYTHON_VERSION);
+ String expectedPath = unixLikePath(python3ux.getAbsolutePath().toLowerCase());
+ String foundPath = unixLikePath(f.getExecutable().toLowerCase());
+ assertThat(foundPath, is(expectedPath));
+ PowerMock.verifyAll();
+ }
+}
diff --git a/swe_checkstyle.xml b/swe_checkstyle.xml
new file mode 100644
index 0000000..47e3ddf
--- /dev/null
+++ b/swe_checkstyle.xml
@@ -0,0 +1,141 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+