| // Copyright 2019 The Bazel Authors. All rights reserved. |
| // |
| // Licensed under the Apache License, Version 2.0 (the "License"); |
| // you may not use this file except in compliance with the License. |
| // You may obtain a copy of the License at |
| // |
| // http://www.apache.org/licenses/LICENSE-2.0 |
| // |
| // Unless required by applicable law or agreed to in writing, software |
| // distributed under the License is distributed on an "AS IS" BASIS, |
| // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| // See the License for the specific language governing permissions and |
| // limitations under the License. |
| |
| package com.google.devtools.build.lib.versioning; |
| |
| import com.google.common.collect.ImmutableList; |
| import com.google.devtools.build.lib.shell.Subprocess; |
| import com.google.devtools.build.lib.shell.SubprocessBuilder; |
| import com.google.devtools.build.lib.shell.SubprocessBuilder.StreamAction; |
| import com.google.devtools.build.lib.vfs.PathFragment; |
| import java.io.ByteArrayOutputStream; |
| import java.io.IOException; |
| import java.io.InputStream; |
| |
| /** |
| * Parser for version strings that comply with the GNU Coding Standards. |
| * |
| * <p>Programs that adhere to the GNU Coding Standards, and many more that do not, recognize a |
| * single {@code --version} flag on their command line that outputs a header line in a specific |
| * format. |
| * |
| * @param <VersionT> the type of the versions that the parser yields |
| */ |
| public class GnuVersionParser<VersionT> { |
| |
| /** The expected program name. */ |
| private final String programName; |
| |
| /** Parser for the expected versioning scheme. */ |
| private final VersionParser<VersionT> versionParser; |
| |
| /** |
| * Constructs a new parser to extract version numbers of a given program. |
| * |
| * @param programName the name of the program expected in the version string |
| * @param versionParser a parser for the versioning semantics of the program |
| */ |
| public GnuVersionParser(String programName, VersionParser<VersionT> versionParser) { |
| this.programName = programName; |
| this.versionParser = versionParser; |
| } |
| |
| /** |
| * Extracts a version number from the given string, which should correspond to the first line of |
| * the output of a program ran with {@code --version}. |
| * |
| * @param input the string to parse |
| * @return the version number extracted from the string |
| * @throws ParseException if the string does not contain the expected program name or if the |
| * version number is invalid |
| */ |
| public VersionT parse(String input) throws ParseException { |
| String prefix = programName + " "; |
| if (!input.startsWith(prefix)) { |
| throw new ParseException("Program name " + programName + " not found"); |
| } |
| return versionParser.parse(input.substring(prefix.length())); |
| } |
| |
| /** |
| * Extracts the first line from an input stream and drains the rest. |
| * |
| * @param input the input stream from which to read |
| * @return the first line from the input stream. Note that it's not possible to distinguish the |
| * case of an empty first line or an empty input stream. |
| * @throws IOException if reading from the input fails for any reason, including encountering an |
| * invalid text encoding |
| */ |
| private static String readFirstLineAndDiscardRest(InputStream input) throws IOException { |
| ByteArrayOutputStream firstLine = new ByteArrayOutputStream(); |
| |
| int b; |
| while ((b = input.read()) != -1 && b != '\n') { |
| firstLine.write(b); |
| } |
| while (input.read() != -1) { |
| // Discard. |
| } |
| input.close(); |
| |
| try { |
| return firstLine.toString("UTF-8"); |
| } catch (IOException e) { |
| throw new IOException("Program output not in UTF-8 format", e); |
| } |
| } |
| |
| /** |
| * Extracts a version number from the given input stream, which should correspond to the output of |
| * a program ran with {@code --version}. This drains the stream. |
| * |
| * @param input the input stream from which to read |
| * @return the version number extracted from the input stream |
| * @throws IOException if reading from the input fails for any reason |
| * @throws ParseException if the first line from the stream does not contain a valid program name |
| * or a valid version number |
| */ |
| public VersionT fromInputStream(InputStream input) throws IOException, ParseException { |
| String firstLine = readFirstLineAndDiscardRest(input); |
| if (firstLine.isEmpty()) { |
| throw new ParseException("No data"); |
| } |
| return parse(firstLine); |
| } |
| |
| /** |
| * Runs a program with {@code --version} and extracts its version number. |
| * |
| * @param program the program to execute |
| * @return the version number extracted from the program |
| * @throws IOException if executing the program or reading from its output fail for any reason |
| * @throws ParseException if the output of the program did not follow the GNU Coding Standard |
| * conventions, or if the printed program name did not match what we expected |
| */ |
| public VersionT fromProgram(PathFragment program) throws IOException, ParseException { |
| Subprocess process = |
| new SubprocessBuilder() |
| .setArgv(ImmutableList.of(program.getPathString(), "--version")) |
| .setStdout(StreamAction.STREAM) |
| .redirectErrorStream(true) |
| .start(); |
| boolean waited = false; |
| try { |
| VersionT version = fromInputStream(process.getInputStream()); |
| process.waitForUninterruptibly(); |
| waited = true; |
| int exitCode = process.exitValue(); |
| if (exitCode != 0) { |
| throw new IOException("Exited with non-zero code " + exitCode); |
| } |
| return version; |
| } finally { |
| if (!waited) { |
| process.destroyAndWait(); |
| } |
| } |
| } |
| } |