diff --git a/src/main/java/de/marw/cmake/cmakecache/CMakeCacheFileParser.java b/src/main/java/de/marw/cmake/cmakecache/CMakeCacheFileParser.java new file mode 100644 index 0000000..c9b1dab --- /dev/null +++ b/src/main/java/de/marw/cmake/cmakecache/CMakeCacheFileParser.java @@ -0,0 +1,193 @@ +/******************************************************************************* + * Copyright (c) 2014 Martin Weber. + * 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: + * Martin Weber - Initial implementation + *******************************************************************************/ + +package de.marw.cmake.cmakecache; + +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.io.LineNumberReader; +import java.text.MessageFormat; +import java.util.Collection; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +/** + * A simple parser for CMake cache files ({@code CMakeCache.txt}). This + * implementation extracts only key-value-pairs corresponding to an entry. It + * does not extract any help texts nor entry types. + * + * @author Martin Weber + */ +public class CMakeCacheFileParser { + + // input line is: key:type=value + private static final Pattern reg = Pattern + .compile("([^=:]*):([^=]*)=(.*[^\t ]|[\t ]*)[\t ]*"); + // input line is: "key":type=value + private static final Pattern regQuoted = Pattern + .compile("\"([^=:]*)\":([^=]*)=(.*[^\t ]|[\t ]*)[\t ]*"); + // input line is: key=value + private static final Pattern regNoType = Pattern + .compile("([^=]*)=(.*[^\t ]|[\t ]*)[\t ]*"); + // input line is: "key"=value + private static final Pattern regQuotedNoType = Pattern + .compile("\"([^=]*)\"=(.*[^\t ]|[\t ]*)[\t ]*"); + + /** + * Parses the content of the specified input stream as a CMake cache file + * content.
+ * This implementation is inspired by cmCacheManager.cxx. + * + * @param is + * the input stream that serves the content of the CMake cache file + * @param filter + * an optional filter for CMake cache file entries or {@code null} if + * all entries are of interest + * @param parsedEntries + * receives the parsed cache file entries. Specify {@code null}, if you + * want to verify the correct syntax of the cache file only. Specify an + * instance of {@link List}, if you expect multiple cache entires of + * the same key int the file. Normally, you would specify an instance + * of {@link Set} here. + * @param errorLog + * receives messages concerning parse errors. Specify {@code null}, if + * you are not interested in error messages. + * @return {@code true} if the file could be parsed without errors, otherwise + * {@code false} + * @throws IOException + * if an operation on the input stream failed + */ + public boolean parse(final InputStream is, final EntryFilter filter, + Collection parsedEntries, List errorLog) + throws IOException { + + final LineNumberReader reader = new LineNumberReader(new InputStreamReader( + is)); + boolean hasErrors = false; + + Map uniqueMap = null; + if (parsedEntries != null && parsedEntries instanceof Set) { + // avoid returning duplicate keys + uniqueMap = new HashMap(); + } + + for (String line; null != (line = reader.readLine());) { + int idx = 0; + // skip leading whitespaces... + for (; idx < line.length(); idx++) { + final char c = line.charAt(idx); + if (!Character.isWhitespace(c)) + break; + } + if (!(idx < line.length())) + continue; // skip blank lines + + if (line.charAt(idx) == '#') + continue; // skip cmake comment lines + + if (idx < line.length()) { + line = line.substring(idx); + + if (line.startsWith("//")) + continue; // ignore help string + + // parse cache entry... + String key = null; + String value = null; + Matcher matcher; + + if ((matcher = reg.matcher(line)).matches() + || (matcher = regQuoted.matcher(line)).matches()) { + // input line is: key:type=value + // input line is: "key":type=value + key = matcher.group(1); + // we do not need the type from group(2) + value = matcher.group(3); + } else if ((matcher = regNoType.matcher(line)).matches() + || (matcher = regQuotedNoType.matcher(line)).matches()) { + // input line is: key=value + // input line is: "key"=value + key = matcher.group(1); + value = matcher.group(2); + } else { + hasErrors |= true; + // add error message + if (errorLog != null) { + final String msg = MessageFormat.format( + "Error: Line {0,number,integer}: Offending entry: {1}", + reader.getLineNumber(), line); + errorLog.add(msg); + } + } + + if (filter != null && parsedEntries != null) { + // no need to call the filter if nothing is to be returned + if (!filter.accept(key)) + continue; // uninteresting entry, get next line + } + + // if value is enclosed in single quotes ('foo') then remove them + // it is used to enclose trailing space or tab + if (key != null && value != null && value.length() >= 2 + && value.charAt(0) == '\'' + && value.charAt(value.length() - 1) == '\'') { + + value = value.substring(1, value.length()); + } + + // store entry + if (parsedEntries != null) { + final SimpleCMakeCacheEntry entry = new SimpleCMakeCacheEntry(key, + value); + if (uniqueMap != null) + uniqueMap.put(key, entry); + else + parsedEntries.add(entry); + } + } + } + if (parsedEntries != null && uniqueMap != null) + parsedEntries.addAll(uniqueMap.values()); + return hasErrors; + } + + //////////////////////////////////////////////////////////////////// + // inner classes + //////////////////////////////////////////////////////////////////// + /** + * A filter for CMake cache file entry keys. + *

+ * Instances of this interface may be passed to the + * {@link CMakeCacheFileParser#CMakeCacheFileParser()} constructor of the + * {@code CMakeCacheFileParser} class. + * + * @author Martin Weber + */ + public interface EntryFilter { + /** + * Tests whether or not the specified entry key should be included in a set + * returned by {@link CMakeCacheFileParser#parse}. + * + * @param key + * The entry key to be tested. Never {@code null} + * @return true if and only if key should be + * included + */ + boolean accept(String key); + } +} diff --git a/src/main/java/de/marw/cmake/cmakecache/SimpleCMakeCacheEntry.java b/src/main/java/de/marw/cmake/cmakecache/SimpleCMakeCacheEntry.java new file mode 100644 index 0000000..0c96219 --- /dev/null +++ b/src/main/java/de/marw/cmake/cmakecache/SimpleCMakeCacheEntry.java @@ -0,0 +1,67 @@ +/******************************************************************************* + * Copyright (c) 2014 Martin Weber. + * 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: + * Martin Weber - Initial implementation + *******************************************************************************/ +package de.marw.cmake.cmakecache; + +/** + * Represents an entry of a CMakeCache.txt file in a simple form: Holds only + * key-value-pairs of an entry. It does not extract any help texts nor entry + * types. + * + * @author Martin Weber + */ +public class SimpleCMakeCacheEntry { + private final String key; + private final String value; + + /** + * @throws IllegalArgumentException + * if {@code key} is empty + * @throws NullPointerException + * if {@code key} is {@code null} or if {@code value} is {@code null} + */ + public SimpleCMakeCacheEntry(String key, String value) { + if (key == null) { + throw new NullPointerException("key"); + } + if (key.length() == 0) { + throw new IllegalArgumentException("key"); + } + if (value == null) { + throw new NullPointerException("value"); + } + + this.value = value; + this.key = key; + } + + /** + * Gets the key property. + * + * @return the current key property. + */ + public String getKey() { + return this.key; + } + + /** + * Gets the value. + * + * @return the current value. + */ + public String getValue() { + return this.value; + } + + public String toString() { + return key + "=" + value; + } + +} \ No newline at end of file diff --git a/src/main/java/de/marw/cmake/cmakecache/SimpleCMakeCacheTxt.java b/src/main/java/de/marw/cmake/cmakecache/SimpleCMakeCacheTxt.java new file mode 100644 index 0000000..83596cc --- /dev/null +++ b/src/main/java/de/marw/cmake/cmakecache/SimpleCMakeCacheTxt.java @@ -0,0 +1,110 @@ +/******************************************************************************* + * Copyright (c) 2015 Martin Weber. + * 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: + * Martin Weber - Initial implementation + *******************************************************************************/ +package de.marw.cmake.cmakecache; + +import java.io.File; +import java.io.FileInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashSet; +import java.util.List; +import java.util.Set; + +/** + * Represents a simplistic subset of the parsed content of a CMake cache file + * (CMakeCache.txt). + * + * @author Martin Weber + */ +public class SimpleCMakeCacheTxt { + + private String buildTool; + private List tools; + private List commands; + + /** + * Creates a new object by parsing the specified file. + * + * @param file + * the file to parse. + * @throws IOException + * if the file could not be read + */ + public SimpleCMakeCacheTxt(File file) throws IOException { + ArrayList tools = new ArrayList(); + ArrayList commands = new ArrayList(); + + // parse CMakeCache.txt... + InputStream is = null; + try { + is = new FileInputStream(file); + final Set entries = new HashSet(); + new CMakeCacheFileParser().parse(is, null, entries, null); + for (SimpleCMakeCacheEntry entry : entries) { + final String toolKey = entry.getKey(); + final String tool = entry.getValue(); + if ("CMAKE_BUILD_TOOL".equals(toolKey)) { + buildTool = tool; + } else if ("CMAKE_COMMAND".equals(toolKey)) { + commands.add(tool); + } else if ("CMAKE_CPACK_COMMAND".equals(toolKey)) { + commands.add(tool); + } else if ("CMAKE_CTEST_COMMAND".equals(toolKey)) { + commands.add(tool); + } else if ("CMAKE_C_COMPILER".equals(toolKey)) { + tools.add(tool); + } else if ("CMAKE_CXX_COMPILER".equals(toolKey)) { + tools.add(tool); + } + } + this.tools = Collections.unmodifiableList(tools); + this.commands = Collections.unmodifiableList(commands); + } finally { + if (is != null) { + try { + is.close(); + } catch (IOException ignore) { + } + } + } + } + + /** + * Gets the name of the tool that processes the generated build scripts. In + * most cases, this method will return the absolute file system path of the + * tool, such as {@code /usr/bin/make}. + * + * @return the CMAKE_BUILD_TOOL entry from the CMakeCache.txt file or + * {@code null} if the file could not be parsed + */ + public String getBuildTool() { + return buildTool; + } + + /** + * Gets the tools that process the source files to binary files (compilers, + * linkers). In most cases, this method will return the absolute file system + * paths of a tool, for example {@code /usr/bin/cc}. + */ + public List getTools() { + return tools; + } + + /** + * Gets the tools provided by CMake itself (cmake, cpack, ctest). In most + * cases, this method will return the absolute file system paths of a tool. + */ + public List getCmakeCommands() { + return commands; + } +} diff --git a/src/main/java/hudson/plugins/cmake/BuildToolEntryParser.java b/src/main/java/hudson/plugins/cmake/BuildToolEntryParser.java new file mode 100644 index 0000000..640aa21 --- /dev/null +++ b/src/main/java/hudson/plugins/cmake/BuildToolEntryParser.java @@ -0,0 +1,77 @@ +/******************************************************************************* + * Copyright (c) 2015 Martin Weber. + * 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: + * Martin Weber - Initial implementation + *******************************************************************************/ +package hudson.plugins.cmake; + +import hudson.FilePath; +import hudson.remoting.VirtualChannel; + +import java.io.BufferedInputStream; +import java.io.File; +import java.io.FileInputStream; +import java.io.IOException; +import java.util.ArrayList; +import java.util.List; + +import jenkins.security.Roles; + +import org.jenkinsci.remoting.RoleChecker; + +import de.marw.cmake.cmakecache.CMakeCacheFileParser; +import de.marw.cmake.cmakecache.CMakeCacheFileParser.EntryFilter; +import de.marw.cmake.cmakecache.SimpleCMakeCacheEntry; + +/** + * Gets the value of the {@code "CMAKE_BUILD_TOOL"} entry from a cmake cache + * file. + * + * @author Martin Weber + */ +public class BuildToolEntryParser implements FilePath.FileCallable { + + private static final long serialVersionUID = 1L; + + /** + * Parses the cach file and returns value of the {@code "CMAKE_BUILD_TOOL"} + * entry. + * + * @return the entry value or {@code null} if the file could not be parsed + */ + @Override + public String invoke(File cmakeCacheFile, VirtualChannel channel) + throws IOException, InterruptedException { + BufferedInputStream is = new BufferedInputStream( + new FileInputStream(cmakeCacheFile)); + List result = new ArrayList( + 1); + final CMakeCacheFileParser parser = new CMakeCacheFileParser(); + + parser.parse(is, new EntryFilter() { + + @Override + public boolean accept(String key) { + return "CMAKE_BUILD_TOOL".equals(key); + } + }, result, null); + if (result.size() > 0) { + return result.get(0).getValue(); + } + return null; + } + + /*- + * @see org.jenkinsci.remoting.RoleSensitive#checkRoles(org.jenkinsci.remoting.RoleChecker) + */ + @Override + public void checkRoles(RoleChecker checker) throws SecurityException { + checker.check(this, Roles.SLAVE); + } + +} diff --git a/src/main/java/hudson/plugins/cmake/CmakeBuilder.java b/src/main/java/hudson/plugins/cmake/CmakeBuilder.java index 63a3f6e..1f7a922 100644 --- a/src/main/java/hudson/plugins/cmake/CmakeBuilder.java +++ b/src/main/java/hudson/plugins/cmake/CmakeBuilder.java @@ -7,6 +7,7 @@ import hudson.Launcher; import hudson.Util; import hudson.model.BuildListener; +import hudson.model.Environment; import hudson.model.AbstractBuild; import hudson.model.AbstractProject; import hudson.model.Computer; @@ -30,11 +31,18 @@ /** * Executes cmake as a build step. * - * @author Volker Kaiser + * @author Volker Kaiser (initial implementation) * @author Martin Weber */ public class CmakeBuilder extends Builder { + /** + * the key for the build variable that holds the build tool that the + * build-scripts have been generated for (e.g. /usr/bin/make or + * /usr/bin/ninja) + */ + public static final String ENV_VAR_NAME_CMAKE_BUILD_TOOL = "CMAKE_BUILD_TOOL"; + private String sourceDir; private String buildDir; private String installDir; @@ -254,6 +262,21 @@ public boolean perform(AbstractBuild build, Launcher launcher, return false; // invokation failed } + /* parse CMakeCache.txt to get the actual build tool */ + FilePath cacheFile = theBuildDir.child("CMakeCache.txt"); + String buildTool = cacheFile.act(new BuildToolEntryParser()); + if (buildTool == null) { + listener.error("Failed to get CMAKE_BUILD_TOOL value from " + + cacheFile.getRemote()); + return false; // abort build + } + // export the variable.. + EnvVars envVars = new EnvVars( + CmakeBuilder.ENV_VAR_NAME_CMAKE_BUILD_TOOL, buildTool); + build.getEnvironments().add(Environment.create(envVars)); + listener.getLogger().println( + "Exported CMAKE_BUILD_TOOL=" + buildTool); + /* invoke make in build dir */ if (0 != launcher .launch() @@ -280,6 +303,7 @@ public boolean perform(AbstractBuild build, Launcher launcher, } } catch (IOException e) { Util.displayIOException(e, listener); + listener.error(e.getLocalizedMessage()); return false; } return true; diff --git a/src/test/java/hudson/plugins/cmake/JobBuildTest.java b/src/test/java/hudson/plugins/cmake/JobBuildTest.java index f29a612..a19cce5 100644 --- a/src/test/java/hudson/plugins/cmake/JobBuildTest.java +++ b/src/test/java/hudson/plugins/cmake/JobBuildTest.java @@ -1,6 +1,10 @@ package hudson.plugins.cmake; +import static org.junit.Assert.assertNotNull; +import hudson.Launcher; +import hudson.model.BuildListener; import hudson.model.FreeStyleBuild; +import hudson.model.AbstractBuild; import hudson.model.FreeStyleProject; import hudson.model.Label; import hudson.model.ParametersDefinitionProperty; @@ -16,6 +20,7 @@ import org.junit.Test; import org.jvnet.hudson.test.JenkinsRule; import org.jvnet.hudson.test.SingleFileSCM; +import org.jvnet.hudson.test.TestBuilder; /** * Tests the CmakeBuilder in a running job. @@ -128,4 +133,45 @@ public void testBuildVariables() throws Exception { } } + /** + * Verifies that the build-tool variable gets injected. + */ + @Test + public void testBuildToolVariableInjected() throws Exception { + FreeStyleProject p = j.createFreeStyleProject(); + p.setScm(scm); + CmakeBuilder cmb = new CmakeBuilder(CmakeTool.DEFAULT, + "Unix Makefiles", "src", "build/Debug", "make"); + cmb.setCleanBuild(true); + cmb.setCleanInstallDir(true); + p.getBuildersList().add(cmb); + GetEnvVarBuilder gevb = new GetEnvVarBuilder(); + p.getBuildersList().add(gevb); + + FreeStyleBuild build = p.scheduleBuild2(0).get(); + System.out.println(JenkinsRule.getLog(build)); + assertNotNull( + CmakeBuilder.ENV_VAR_NAME_CMAKE_BUILD_TOOL, + gevb.value); + } + + // ////////////////////////////////////////////////////////////////// + // inner classes + // ////////////////////////////////////////////////////////////////// + private static class GetEnvVarBuilder extends TestBuilder { + String value; + + /*- + * @see org.jvnet.hudson.test.TestBuilder#perform(hudson.model.AbstractBuild, hudson.Launcher, hudson.model.BuildListener) + */ + @Override + public boolean perform(AbstractBuild build, Launcher launcher, + BuildListener listener) throws InterruptedException, + IOException { + value = build.getEnvironment(listener).get( + CmakeBuilder.ENV_VAR_NAME_CMAKE_BUILD_TOOL); + return true; + } + + } }