/******************************************************************************* * Copyright (c) 2017 Synopsys, Inc * All rights reserved. This program and the accompanying materials * are made available under the terms of the Eclipse Public License v1.0 * which accompanies this distribution, and is available at * http://www.eclipse.org/legal/epl-v10.html * * Contributors: * Synopsys, Inc - initial implementation and documentation *******************************************************************************/ package jenkins.plugins.coverity; import hudson.Extension; import hudson.FilePath; import hudson.Launcher; import hudson.model.*; import hudson.EnvVars; import hudson.model.Queue; import hudson.remoting.Channel; import hudson.remoting.VirtualChannel; import hudson.util.ArgumentListBuilder; import org.apache.commons.lang.StringUtils; import org.apache.commons.lang.Validate; import org.apache.tools.ant.types.Commandline; import java.io.*; import java.lang.reflect.Array; import java.util.*; import java.util.logging.Logger; import java.util.regex.Matcher; import java.util.regex.Pattern; public class CoverityUtils { private static final Logger logger = Logger.getLogger(CoverityUtils.class.getName()); /** * Evaluates an environment variable using the specified parser. The result is an interpolated string. */ public static String evaluateEnvVars(String input, EnvVars environment, boolean useAdvancedParser)throws RuntimeException{ try{ if(useAdvancedParser){ String interpolated = EnvParser.interpolateRecursively(input, 1, environment); return interpolated; } else { return environment.expand(input); } }catch(Exception e){ throw new RuntimeException("Error trying to evaluate environment variable: " + input); } } public static void checkDir(VirtualChannel channel, String home) throws Exception { Validate.notNull(channel, VirtualChannel.class.getName() + " object can't be null"); Validate.notNull(home, String.class.getName() + " object can't be null"); FilePath homePath = new FilePath(channel, home); if(!homePath.exists()){ throw new Exception("Directory: " + home + " doesn't exist."); } } /** * getCovBuild * * Retrieves the location of cov-build executable/sh from the system and returns the string of the * path * @return string of cov-build's path */ public static String getCovBuild(TaskListener listener, Node node) { AbstractBuild build = getBuild(); AbstractProject project = build.getProject(); CoverityPublisher publisher = (CoverityPublisher) project.getPublishersList().get(CoverityPublisher.class); InvocationAssistance invocationAssistance = publisher.getInvocationAssistance(); if(listener == null){ try{ throw new Exception("Listener used by getCovBuild() is null."); } catch (Exception e){ e.printStackTrace(); return null; } } String covBuild = "cov-build"; String home = null; try { home = publisher.getDescriptor().getHome(node, build.getEnvironment(listener)); } catch(Exception e) { StringWriter sw = new StringWriter(); PrintWriter pw = new PrintWriter(sw); e.printStackTrace(pw); listener.getLogger().println("[Error] " + pw.toString()); e.printStackTrace(); } if(invocationAssistance != null){ if(invocationAssistance.getSaOverride() != null) { try { home = new CoverityInstallation(invocationAssistance.getSaOverride()).forEnvironment(build.getEnvironment(listener)).getHome(); CoverityUtils.checkDir(node.getChannel(), home); } catch(IOException e) { e.printStackTrace(); } catch(InterruptedException e) { e.printStackTrace(); } catch(Exception e) { e.printStackTrace(); } } } if(home != null) { covBuild = new FilePath(node.getChannel(), home).child("bin").child(covBuild).getRemote(); } return covBuild; } /** * Calls intepolate() in order to evaluate environment variables. In the case that a substitution took place, it * tokenize the result and it calls itself on each token in case further evaluations are needed. * * In the case of a recursive definition (ex: VAR1=$VAR2, VAR2=$VAR1) an exception is thrown. */ public static List<String> expand(String input, EnvVars environment) throws ParseException { /** * Interpolates environment */ List<String> output = new ArrayList<>(); String interpolated = EnvParser.interpolateRecursively(input, 1, environment); output.addAll(EnvParser.tokenize(interpolated)); return output; } /** * Evaluates environment variables on a command represented by a list of tokens. */ public static List<String> evaluateEnvVars(List<String> input, EnvVars environment){ List<String> output = new ArrayList<String>(); try { for(String arg : input){ output.addAll(expand(arg, environment)); } } catch(ParseException e){ throw new RuntimeException(e.getMessage()); } return output; } /** * Gets the stacktrace from an exception, so that this exception can be handled. */ public static String getStackTrace(Exception e){ StringWriter writer = new StringWriter(); PrintWriter printWriter = new PrintWriter( writer ); e.printStackTrace(printWriter); printWriter.flush(); String stackTrace = writer.toString(); try { writer.close(); printWriter.close(); } catch (IOException e1) { } return stackTrace; } public static void handleException(String message, AbstractBuild<?, ?> build, BuildListener listener, Exception exception){ listener.getLogger().println(message); listener.getLogger().println("Stacktrace: \n" + CoverityUtils.getStackTrace(exception)); build.setResult(Result.FAILURE); } public static void handleException(String message, AbstractBuild<?, ?> build, TaskListener listener, Exception exception){ listener.getLogger().println(message); listener.getLogger().println("Stacktrace: \n" + CoverityUtils.getStackTrace(exception)); build.setResult(Result.FAILURE); } /** * Prepares command according with the specified parsing mechanism. If "useAdvancedParser" is set to true, the plugin * will evaluate environment variables with its custom mechanism. If not, environment variable substitution will be * handled by Jenkins in the standard way. */ public static List<String> prepareCmds(List<String> input, String[] envVarsArray, boolean useAdvancedParser){ if(useAdvancedParser){ EnvVars envVars = new EnvVars(arrayToMap(envVarsArray)); return CoverityUtils.evaluateEnvVars(input, envVars); } else { return input; } } public static List<String> prepareCmds(List<String> input, EnvVars envVars, boolean useAdvancedParser){ if(useAdvancedParser){ return CoverityUtils.evaluateEnvVars(input, envVars); } else { return input; } } /** * Jenkins API ProcStarter.envs() returns an array of environment variables where each element is a string "key=value". * However the constructor for EnvVars accepts only arrays of the format [key1, value1, key2, value2]. Because of * this, we need to transform that array into a map and use a constructor that accepts that map. */ public static Map<String, String> arrayToMap(String[] input){ Map<String, String> result = new HashMap<String, String>(); for(int i=0; i<input.length; i++){ List<String> keyValuePair = splitKeyValue(input[i]); if(keyValuePair != null && !keyValuePair.isEmpty()){ String key = keyValuePair.get(0); String value = keyValuePair.get(1); result.put(key, value); } } return result; } /** * Split string of the form key=value into an array [key, value] */ public static List<String> splitKeyValue(String input){ if(input == null){ return null; } List<String> result = new ArrayList<>(); int index = input.indexOf('='); if(index == 0){ logger.warning("Could not parse environment variable \"" + input + "\" because its key is empty."); } else if (index < 0){ logger.warning("Could not parse environment variable \"" + input + "\" because no value for it has been defined."); } else if(index == input.length() - 1){ logger.warning("Could not parse environment variable \"" + input + "\" because the value for it is empty."); } else { String key = input.substring(0, index); String value = input.substring(index + 1); result.add(key); result.add(value); } return result; } /** * Returns the InvocationAssistance for a given build. */ public static InvocationAssistance getInvocationAssistance(AbstractBuild<?, ?> build){ AbstractProject project = build.getProject(); CoverityPublisher publisher = (CoverityPublisher) project.getPublishersList().get(CoverityPublisher.class); return publisher.getInvocationAssistance(); } /** * Returns the InvocationAssistance on the current thread. This can be used when an "AbstractBuild" object is not * available, for example while decorating the launcher. */ public static InvocationAssistance getInvocationAssistance(){ AbstractBuild build = getBuild(); return getInvocationAssistance(build); } /** * Collects environment variables from an array and an EnvVars object and returns an updated EnvVars object. * This is useful for updating the environment variables on a ProcStarter with the variables from the listener. */ public static String[] addEnvVars(String[] envVarsArray, EnvVars envVars){ // All variables are stored on a map, the ones from ProcStarter will take precedence. EnvVars resultMap = new EnvVars(envVars); resultMap.putAll(arrayToMap(envVarsArray)); String[] r = new String[resultMap.size()]; int idx=0; for (Map.Entry<String,String> e : resultMap.entrySet()) { r[idx++] = e.getKey() + '=' + e.getValue(); } return r; } public static int runCmd(List<String> cmd, AbstractBuild<?, ?> build, Launcher launcher, TaskListener listener, EnvVars envVars, boolean useAdvancedParser) throws IOException, InterruptedException { /** * Get environment variables from a launcher, add custom environment environment variables if needed, * then call join() to starts the launcher process. */ String[] launcherEnvVars = launcher.launch().envs(); launcherEnvVars = CoverityUtils.addEnvVars(launcherEnvVars, envVars); cmd = prepareCmds(cmd, launcherEnvVars, useAdvancedParser); int result = launcher. launch(). cmds(new ArgumentListBuilder(cmd.toArray(new String[cmd.size()]))). pwd(build.getWorkspace()). stdout(listener). stderr(listener.getLogger()). envs(launcherEnvVars). join(); return result; } public static AbstractBuild getBuild(){ Executor executor = Executor.currentExecutor(); Queue.Executable exec = executor.getCurrentExecutable(); AbstractBuild build = (AbstractBuild) exec; return build; } public static String doubleQuote(String input){ return "\"" + input + "\""; } /** * Coverity's parser remove double/single quotes but Jenkins parser does not. When dealing (for instance) with * streams with spaces, we would expect [--stream, My Stream]. In order to do this the token "My Stream" must be * quoted if using out parser, but not if using Jenkins. */ public static String doubleQuote(String input, boolean useAdvancedParser){ if(useAdvancedParser){ return "\"" + input + "\""; } else { return input; } } /** * Gets environment variables from the build. */ public static EnvVars getBuildEnvVars(TaskListener listener){ AbstractBuild build = CoverityUtils.getBuild(); EnvVars envVars = null; try { envVars = build.getEnvironment(listener); } catch (Exception e) { CoverityUtils.handleException(e.getMessage(), build, listener, e); } return envVars; } /** * Gets environment variables from the given build */ public static EnvVars getBuildEnvVars(AbstractBuild build, TaskListener listener){ EnvVars envVars = null; try { envVars = build.getEnvironment(listener); } catch (Exception e) { CoverityUtils.handleException(e.getMessage(), build, listener, e); } return envVars; } public static Collection<File> listFiles( File directory, FilenameFilter filter, boolean recurse) { Vector<File> files = new Vector<File>(); File[] entries = directory.listFiles(); if (entries == null) { return files; } for(File entry : entries) { if(filter == null || filter.accept(directory, entry.getName())) { files.add(entry); } if(recurse && entry.isDirectory()) { files.addAll(listFiles(entry, filter, recurse)); } } return files; } public static File[] listFilesAsArray( File directory, FilenameFilter filter, boolean recurse) { Collection<File> files = listFiles(directory, filter, recurse); File[] arr = new File[files.size()]; return files.toArray(arr); } }