blob: 962f350558538c1366cd5fd25655d1ca90aa76c3 [file] [log] [blame]
// 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();
}
}
}
}