blob: de5041d2f64c13ca988da1cc4204131d5595bc36 [file] [log] [blame]
// Copyright 2015 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.rules.apple;
import com.google.common.base.Joiner;
import com.google.common.base.Preconditions;
import com.google.common.base.Splitter;
import com.google.common.base.Strings;
import com.google.common.collect.ComparisonChain;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.Ordering;
import com.google.devtools.build.lib.concurrent.ThreadSafety.Immutable;
import com.google.devtools.build.lib.skyframe.serialization.autocodec.AutoCodec;
import com.google.devtools.build.lib.skylarkbuildapi.apple.DottedVersionApi;
import com.google.devtools.build.lib.skylarkinterface.SkylarkPrinter;
import java.util.ArrayList;
import java.util.Objects;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import javax.annotation.Nullable;
/**
* Represents Xcode versions and allows parsing them.
*
* <p>Xcode versions are formed of multiple components, separated by periods, for example {@code
* 4.5.6} or {@code 5.0.1beta2}. Components must start with a non-negative integer and at least one
* component must be present.
*
* <p>Specifically, the format of a component is {@code \d+([a-z0-9]*?)?(\d+)?}.
*
* <p>If this smells a lot like semver, it does, but Xcode versions are sometimes special. This is
* why this class is in the {@code apple} package and has to remain as such.
*
* <p>Dotted versions are ordered using natural integer sorting on components in order from first to
* last where any missing element is considered to have the value 0 if they don't contain any
* non-numeric characters. For example:
*
* <pre>
* 3.1.25 > 3.1.1
* 3.1.20 > 3.1.2
* 3.1.1 > 3.1
* 3.1 == 3.1.0.0
* 3.2 > 3.1.8
* </pre>
*
* <p>If the component contains any alphabetic characters after the leading integer, it is
* considered <strong>smaller</strong> than any components with the same integer but larger than any
* component with a smaller integer. If the integers are the same, the alphabetic sequences are
* compared lexicographically, and if <i>they</i> turn out to be the same, the final (optional)
* integer is compared. As with the leading integer, this final integer is considered to be 0 if not
* present. For example:
*
* <pre>
* 3.1.1 > 3.1.1beta3
* 3.1.1beta1 > 3.1.0
* 3.1 > 3.1.0alpha1
*
* 3.1.0beta0 > 3.1.0alpha5.6
* 3.4.2alpha2 > 3.4.2alpha1
* 3.4.2alpha2 > 3.4.2alpha1.5
* 3.1alpha1 > 3.1alpha
* </pre>
*
* <p>This class is immutable and can safely be shared among threads.
*/
@Immutable
@AutoCodec
public final class DottedVersion implements DottedVersionApi<DottedVersion> {
/** Wrapper class for {@link DottedVersion} whose {@link #equals(Object)} method is string
* equality.
*
* <p>This is necessary because Bazel assumes that
* {@link com.google.devtools.build.lib.analysis.config.FragmentOptions} that are equal yield
* fragments that are the same. However, this does not hold if the options hold a
* {@link DottedVersion} because trailing zeroes are not considered significant when comparing
* them, but they do matter in configuration fragments (for example, they end up in output
* directory names)</p>
* */
@Immutable
public static final class Option {
private final DottedVersion version;
private Option(DottedVersion version) {
this.version = Preconditions.checkNotNull(version);
}
public DottedVersion get() {
return version;
}
@Override
public int hashCode() {
return version.stringRepresentation.hashCode();
}
@Override
public boolean equals(Object o) {
if (this == o) {
return true;
}
if (!(o instanceof Option)) {
return false;
}
return version.stringRepresentation.equals(((Option) o).version.stringRepresentation);
}
}
public static DottedVersion maybeUnwrap(DottedVersion.Option option) {
return option != null ? option.get() : null;
}
public static Option option(DottedVersion version) {
return version == null ? null : new Option(version);
}
private static final Splitter DOT_SPLITTER = Splitter.on('.');
private static final Pattern COMPONENT_PATTERN =
Pattern.compile("(\\d+)([a-z0-9]*?)?(\\d+)?", Pattern.CASE_INSENSITIVE);
private static final String ILLEGAL_VERSION =
"Dotted version components must all be of the form \\d+([a-z0-9]*?)?(\\d+)? but got '%s'";
private static final String NO_ALPHA_SEQUENCE = null;
private static final Component ZERO_COMPONENT = new Component(0, NO_ALPHA_SEQUENCE, 0, "0");
/** Exception thrown when parsing an invalid dotted version. */
public static class InvalidDottedVersionException extends Exception {
InvalidDottedVersionException(String msg) {
super(msg);
}
InvalidDottedVersionException(String msg, Throwable cause) {
super(msg, cause);
}
}
/**
* Create a dotted version by parsing the given version string. Throws an unchecked exception if
* the argument is malformed.
*/
public static DottedVersion fromStringUnchecked(String version) {
try {
return fromString(version);
} catch (InvalidDottedVersionException e) {
throw new IllegalArgumentException(e);
}
}
/**
* Generates a new dotted version from the given version string.
*
* @throws InvalidDottedVersionException if the passed string is not a valid dotted version
*/
public static DottedVersion fromString(String version) throws InvalidDottedVersionException {
if (Strings.isNullOrEmpty(version)) {
throw new InvalidDottedVersionException(String.format(ILLEGAL_VERSION, version));
}
ArrayList<Component> components = new ArrayList<>();
for (String component : DOT_SPLITTER.split(version)) {
components.add(toComponent(component, version));
}
int numOriginalComponents = components.size();
// Remove trailing (but not the first or middle) zero components for easier comparison and
// hashcoding.
for (int i = components.size() - 1; i > 0; i--) {
if (components.get(i).equals(ZERO_COMPONENT)) {
components.remove(i);
} else {
break;
}
}
return new DottedVersion(ImmutableList.copyOf(components), version, numOriginalComponents);
}
private static Component toComponent(String component, String version)
throws InvalidDottedVersionException {
Matcher parsedComponent = COMPONENT_PATTERN.matcher(component);
if (!parsedComponent.matches()) {
throw new InvalidDottedVersionException(String.format(ILLEGAL_VERSION, version));
}
int firstNumber;
String alphaSequence = NO_ALPHA_SEQUENCE;
int secondNumber = 0;
firstNumber = parseNumber(parsedComponent, 1, version);
if (!Strings.isNullOrEmpty(parsedComponent.group(2))) {
alphaSequence = parsedComponent.group(2);
}
if (!Strings.isNullOrEmpty(parsedComponent.group(3))) {
secondNumber = parseNumber(parsedComponent, 3, version);
}
return new Component(firstNumber, alphaSequence, secondNumber, component);
}
private static int parseNumber(Matcher parsedComponent, int group, String version)
throws InvalidDottedVersionException {
int firstNumber;
try {
firstNumber = Integer.parseInt(parsedComponent.group(group));
} catch (NumberFormatException e) {
throw new InvalidDottedVersionException(String.format(ILLEGAL_VERSION, version), e);
}
return firstNumber;
}
private final ImmutableList<Component> components;
private final String stringRepresentation;
private final int numOriginalComponents;
@AutoCodec.VisibleForSerialization
DottedVersion(
ImmutableList<Component> components, String stringRepresentation, int numOriginalComponents) {
this.components = components;
this.stringRepresentation = stringRepresentation;
this.numOriginalComponents = numOriginalComponents;
}
@Override
public int compareTo(DottedVersion other) {
int maxComponents = Math.max(components.size(), other.components.size());
for (int componentIndex = 0; componentIndex < maxComponents; componentIndex++) {
Component myComponent = getComponent(componentIndex);
Component otherComponent = other.getComponent(componentIndex);
int comparison = myComponent.compareTo(otherComponent);
if (comparison != 0) {
return comparison;
}
}
return 0;
}
@Override
public int compareTo_skylark(DottedVersion other) {
return compareTo(other);
}
/**
* Returns the string representation of this dotted version, padded or truncated to the specified
* number of components.
*
* <p>For example, a dotted version of "7.3.0" will return "7" if one is requested, "7.3" if two
* are requested, "7.3.0" if three are requested, and "7.3.0.0" if four are requested.
*
* @param numComponents a positive number of dot-separated numbers that should be present in the
* returned string representation
*/
public String toStringWithComponents(int numComponents) {
Preconditions.checkArgument(numComponents > 0,
"Can't serialize as a version with %s components", numComponents);
ImmutableList.Builder<Component> stringComponents = ImmutableList.builder();
if (numComponents <= components.size()) {
stringComponents.addAll(components.subList(0, numComponents));
} else {
stringComponents.addAll(components);
for (int i = components.size(); i < numComponents; i++) {
stringComponents.add(ZERO_COMPONENT);
}
}
return Joiner.on('.').join(stringComponents.build());
}
/**
* Returns the string representation of this dotted version, padded to a minimum number of
* components if the string representation does not already contain that many components.
*
* <p>For example, a dotted version of "7.3" will return "7.3" with either one or two components
* requested, "7.3.0" if three are requested, and "7.3.0.0" if four are requested.
*
* <p>Trailing zero components at the end of a string representation will not be removed. For
* example, a dotted version of "1.0.0" will return "1.0.0" if only one or two components are
* requested.
*
* @param numMinComponents the minimum number of dot-separated numbers that should be present in
* the returned string representation
*/
public String toStringWithMinimumComponents(int numMinComponents) {
return toStringWithComponents(Math.max(this.numOriginalComponents, numMinComponents));
}
/**
* Returns true if this version number has any alphabetic characters, such as 'alpha' in
* "7.3alpha.2".
*/
public boolean hasAlphabeticCharacters() {
for (Component component : components) {
if (!Objects.equals(component.alphaSequence, NO_ALPHA_SEQUENCE)) {
return true;
}
}
return false;
}
/**
* Returns the number of components in this version number. For example, "7.3.0" has three
* components.
*/
public int numComponents() {
return components.size();
}
@Override
public String toString() {
return stringRepresentation;
}
@Override
public boolean equals(Object other) {
if (this == other) {
return true;
}
if (other == null || getClass() != other.getClass()) {
return false;
}
return compareTo((DottedVersion) other) == 0;
}
@Override
public int hashCode() {
return Objects.hash(components);
}
private Component getComponent(int groupIndex) {
if (components.size() > groupIndex) {
return components.get(groupIndex);
}
return ZERO_COMPONENT;
}
@Override
public void repr(SkylarkPrinter printer) {
printer.append(stringRepresentation);
}
@AutoCodec.VisibleForSerialization
@AutoCodec
static final class Component implements Comparable<Component> {
private final int firstNumber;
@Nullable private final String alphaSequence;
private final int secondNumber;
private final String stringRepresentation;
@AutoCodec.VisibleForSerialization
Component(
int firstNumber,
@Nullable String alphaSequence,
int secondNumber,
String stringRepresentation) {
this.firstNumber = firstNumber;
this.alphaSequence = alphaSequence;
this.secondNumber = secondNumber;
this.stringRepresentation = stringRepresentation;
}
@Override
public int compareTo(Component other) {
return ComparisonChain.start()
.compare(firstNumber, other.firstNumber)
.compare(alphaSequence, other.alphaSequence, Ordering.natural().nullsLast())
.compare(secondNumber, other.secondNumber)
.result();
}
@Override
public boolean equals(Object other) {
if (this == other) {
return true;
}
if (other == null || getClass() != other.getClass()) {
return false;
}
return compareTo((Component) other) == 0;
}
@Override
public int hashCode() {
return Objects.hash(firstNumber, alphaSequence, secondNumber);
}
@Override
public String toString() {
return stringRepresentation;
}
}
}