blob: 8c2a0689d1e26b1129d6a70ffb252445fbc0a2ce [file] [log] [blame]
// Copyright 2014 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.skyframe;
import static com.google.devtools.build.lib.actions.MiddlemanType.RUNFILES_MIDDLEMAN;
import com.google.common.base.MoreObjects;
import com.google.common.base.Preconditions;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.Interner;
import com.google.devtools.build.lib.actions.Action;
import com.google.devtools.build.lib.actions.ActionAnalysisMetadata;
import com.google.devtools.build.lib.actions.ActionExecutionException;
import com.google.devtools.build.lib.actions.ActionLookupData;
import com.google.devtools.build.lib.actions.ActionLookupKey;
import com.google.devtools.build.lib.actions.ActionLookupValue;
import com.google.devtools.build.lib.actions.ActionTemplate;
import com.google.devtools.build.lib.actions.Artifact;
import com.google.devtools.build.lib.actions.Artifact.DerivedArtifact;
import com.google.devtools.build.lib.actions.Artifact.SpecialArtifact;
import com.google.devtools.build.lib.actions.Artifact.TreeFileArtifact;
import com.google.devtools.build.lib.actions.FileArtifactValue;
import com.google.devtools.build.lib.actions.FileValue;
import com.google.devtools.build.lib.actions.FilesetTraversalParams.DirectTraversalRoot;
import com.google.devtools.build.lib.actions.FilesetTraversalParams.PackageBoundaryMode;
import com.google.devtools.build.lib.bugreport.BugReport;
import com.google.devtools.build.lib.cmdline.Label;
import com.google.devtools.build.lib.concurrent.BlazeInterners;
import com.google.devtools.build.lib.events.Event;
import com.google.devtools.build.lib.server.FailureDetails.Execution;
import com.google.devtools.build.lib.server.FailureDetails.Execution.Code;
import com.google.devtools.build.lib.server.FailureDetails.FailureDetail;
import com.google.devtools.build.lib.skyframe.ActionTemplateExpansionValue.ActionTemplateExpansionKey;
import com.google.devtools.build.lib.skyframe.RecursiveFilesystemTraversalFunction.RecursiveFilesystemTraversalException;
import com.google.devtools.build.lib.skyframe.RecursiveFilesystemTraversalValue.ResolvedFile;
import com.google.devtools.build.lib.util.DetailedExitCode;
import com.google.devtools.build.lib.util.Fingerprint;
import com.google.devtools.build.lib.vfs.PathFragment;
import com.google.devtools.build.lib.vfs.RootedPath;
import com.google.devtools.build.lib.vfs.XattrProvider;
import com.google.devtools.build.skyframe.SkyFunction;
import com.google.devtools.build.skyframe.SkyFunctionException;
import com.google.devtools.build.skyframe.SkyFunctionException.Transience;
import com.google.devtools.build.skyframe.SkyKey;
import com.google.devtools.build.skyframe.SkyValue;
import com.google.devtools.build.skyframe.SkyframeLookupResult;
import java.io.IOException;
import java.util.Map;
import java.util.function.Supplier;
import javax.annotation.Nullable;
/**
* A builder of values for {@link Artifact} keys when the key is not a simple generated artifact. To
* save memory, ordinary generated artifacts (non-middleman, non-tree) have their metadata accessed
* directly from the corresponding {@link ActionExecutionValue}. This SkyFunction is therefore only
* usable for source, middleman, and tree artifacts.
*/
public final class ArtifactFunction implements SkyFunction {
private final Supplier<Boolean> mkdirForTreeArtifacts;
private final MetadataConsumerForMetrics sourceArtifactsSeen;
private final XattrProvider xattrProvider;
/** A {@link SkyValue} representing a missing input file. */
public static final class MissingArtifactValue implements SkyValue {
private final DetailedExitCode detailedExitCode;
private MissingArtifactValue(Artifact missingArtifact) {
FailureDetail failureDetail =
FailureDetail.newBuilder()
.setMessage(constructErrorMessage(missingArtifact, "missing input file"))
.setExecution(Execution.newBuilder().setCode(Code.SOURCE_INPUT_MISSING))
.build();
this.detailedExitCode = DetailedExitCode.of(failureDetail);
}
DetailedExitCode getDetailedExitCode() {
return detailedExitCode;
}
@Override
public String toString() {
return MoreObjects.toStringHelper(this).add("detailedExitCode", detailedExitCode).toString();
}
}
public ArtifactFunction(
Supplier<Boolean> mkdirForTreeArtifacts,
MetadataConsumerForMetrics sourceArtifactsSeen,
XattrProvider xattrProvider) {
this.mkdirForTreeArtifacts = mkdirForTreeArtifacts;
this.sourceArtifactsSeen = sourceArtifactsSeen;
this.xattrProvider = xattrProvider;
}
@Nullable
@Override
public SkyValue compute(SkyKey skyKey, Environment env)
throws ArtifactFunctionException, InterruptedException {
Artifact artifact = (Artifact) skyKey;
if (!artifact.hasKnownGeneratingAction()) {
// If the artifact has no known generating action, it is a source artifact.
return createSourceValue(artifact, env);
}
Artifact.DerivedArtifact derivedArtifact = (DerivedArtifact) artifact;
ArtifactDependencies artifactDependencies =
ArtifactDependencies.discoverDependencies(derivedArtifact, env);
if (artifactDependencies == null) {
return null;
}
// If the action is an ActionTemplate, we need to expand the ActionTemplate into concrete
// actions, execute those actions in parallel and then aggregate the action execution results.
ActionTemplate<?> actionTemplate = artifactDependencies.maybeGetTemplateActionForTreeArtifact();
if (actionTemplate != null) {
if (mkdirForTreeArtifacts.get()) {
mkdirForTreeArtifact(artifact, env, actionTemplate);
}
return createTreeArtifactValueFromActionKey(artifactDependencies, env);
}
ActionLookupData generatingActionKey = derivedArtifact.getGeneratingActionKey();
ActionExecutionValue actionValue = (ActionExecutionValue) env.getValue(generatingActionKey);
if (actionValue == null) {
return null;
}
if (artifact.isTreeArtifact()) {
// We got a request for the whole tree artifact. We can just return the associated
// TreeArtifactValue.
return Preconditions.checkNotNull(actionValue.getTreeArtifactValue(artifact), artifact);
}
Preconditions.checkState(artifact.isMiddlemanArtifact(), artifact);
Action action =
Preconditions.checkNotNull(
artifactDependencies.actionLookupValue.getAction(generatingActionKey.getActionIndex()),
"Null middleman action? %s",
artifactDependencies);
FileArtifactValue individualMetadata = actionValue.getExistingFileArtifactValue(artifact);
if (isAggregatingValue(action)) {
return createRunfilesArtifactValue(artifact, action, individualMetadata, env);
}
return individualMetadata;
}
private static void mkdirForTreeArtifact(
Artifact artifact, Environment env, ActionTemplate<?> actionForFailure)
throws ArtifactFunctionException {
try {
artifact.getPath().createDirectoryAndParents();
} catch (IOException e) {
String errorMessage =
String.format(
"Failed to create output directory for TreeArtifact %s: %s",
artifact.getExecPath(), e.getMessage());
env.getListener()
.handle(Event.error(actionForFailure.getOwner().getLocation(), errorMessage));
// We could throw this as an IOException and expect our callers to catch and reprocess it,
// but we know the action at fault, so we should be in charge.
DetailedExitCode code =
DetailedExitCode.of(
FailureDetail.newBuilder()
.setMessage(errorMessage)
.setExecution(
Execution.newBuilder().setCode(Code.TREE_ARTIFACT_DIRECTORY_CREATION_FAILURE))
.build());
throw new ArtifactFunctionException(
new ActionExecutionException(errorMessage, e, actionForFailure, false, code));
}
}
@SuppressWarnings("LenientFormatStringValidation")
@Nullable
private static TreeArtifactValue createTreeArtifactValueFromActionKey(
ArtifactDependencies artifactDependencies, Environment env) throws InterruptedException {
// Request the list of expanded action keys from the ActionTemplate.
ImmutableList<ActionLookupData> expandedActionExecutionKeys =
artifactDependencies.getActionTemplateExpansionKeys(env);
if (expandedActionExecutionKeys == null) {
return null; // The expanded actions are not yet available.
}
SkyframeLookupResult expandedActionValues =
env.getValuesAndExceptions(expandedActionExecutionKeys);
if (env.valuesMissing()) {
return null; // The execution values of the expanded actions are not yet all available.
}
// Aggregate the metadata for individual TreeFileArtifacts into a TreeArtifactValue for the
// parent TreeArtifact.
SpecialArtifact parent = (SpecialArtifact) artifactDependencies.artifact;
TreeArtifactValue.Builder treeBuilder = TreeArtifactValue.newBuilder(parent);
boolean omitted = false;
for (ActionLookupData actionKey : expandedActionExecutionKeys) {
boolean sawTreeChild = false;
ActionExecutionValue actionExecutionValue =
(ActionExecutionValue) expandedActionValues.get(actionKey);
if (actionExecutionValue == null) {
return null;
}
for (Map.Entry<Artifact, FileArtifactValue> entry :
actionExecutionValue.getAllFileValues().entrySet()) {
Artifact artifact = entry.getKey();
Preconditions.checkState(
artifact.hasParent(),
"Parentless artifact %s found in ActionExecutionValue for %s: %s %s",
artifact,
actionKey,
actionExecutionValue,
artifactDependencies);
if (artifact.getParent().equals(parent)) {
sawTreeChild = true;
if (FileArtifactValue.OMITTED_FILE_MARKER.equals(entry.getValue())) {
omitted = true;
} else {
treeBuilder.putChild((TreeFileArtifact) artifact, entry.getValue());
}
}
}
Preconditions.checkState(
sawTreeChild,
"Action denoted by %s does not output any TreeFileArtifacts from %s",
actionKey,
artifactDependencies);
}
TreeArtifactValue tree = treeBuilder.build();
if (omitted) {
// Expected 1 args, but got 2.
Preconditions.checkState(
tree.getChildValues().isEmpty(),
"Action template expansion has some but not all outputs omitted, present outputs: %s",
artifactDependencies,
tree.getChildValues());
return TreeArtifactValue.OMITTED_TREE_MARKER;
}
return tree;
}
@Nullable
private SkyValue createSourceValue(Artifact artifact, Environment env)
throws InterruptedException, ArtifactFunctionException {
RootedPath path = RootedPath.toRootedPath(artifact.getRoot().getRoot(), artifact.getPath());
SkyKey fileSkyKey = FileValue.key(path);
FileValue fileValue;
try {
fileValue = (FileValue) env.getValueOrThrow(fileSkyKey, IOException.class);
} catch (IOException e) {
throw new ArtifactFunctionException(
SourceArtifactException.create(artifact, e), Transience.PERSISTENT);
}
if (fileValue == null) {
return null;
}
if (!fileValue.exists()) {
return new MissingArtifactValue(artifact);
}
if (fileValue.isDirectory()) {
env.getListener().post(SourceDirectoryEvent.create(artifact.getExecPath()));
}
if (!fileValue.isDirectory() || !TrackSourceDirectoriesFlag.trackSourceDirectories()) {
FileArtifactValue metadata;
try {
metadata = FileArtifactValue.createForSourceArtifact(artifact, fileValue, xattrProvider);
} catch (IOException e) {
throw new ArtifactFunctionException(
SourceArtifactException.create(artifact, e), Transience.TRANSIENT);
}
sourceArtifactsSeen.accumulate(metadata);
return metadata;
}
// For directory artifacts that are not Filesets, we initiate a directory traversal here, and
// compute a hash from the directory structure.
// We rely on the guarantees of RecursiveFilesystemTraversalFunction for correctness.
//
// This approach may have unexpected interactions with --package_path. In particular, the exec
// root is set up from the loading / analysis phase, and it is now too late to change it;
// therefore, this may traverse a different set of files depending on which targets are built
// at the same time and what the package-path layout is (this may be moot if there is only one
// entry). Or this may return a set of files that's inconsistent with those actually available
// to the action (for local execution).
//
// In the future, we need to make this result the source of truth for the files available to
// the action so that we at least have consistency.
TraversalRequest request =
DirectoryArtifactTraversalRequest.create(
DirectTraversalRoot.forRootedPath(path), /*skipTestingForSubpackage=*/ true, artifact);
RecursiveFilesystemTraversalValue value;
try {
value =
(RecursiveFilesystemTraversalValue)
env.getValueOrThrow(request, RecursiveFilesystemTraversalException.class);
} catch (RecursiveFilesystemTraversalException e) {
// Use a switch to guarantee that if a new type is added, this stops compiling.
switch (e.getType()) {
case DANGLING_SYMLINK:
case FILE_OPERATION_FAILURE:
case SYMLINK_CYCLE_OR_INFINITE_EXPANSION:
throw new ArtifactFunctionException(
SourceArtifactException.create(artifact, e), Transience.PERSISTENT);
case CANNOT_CROSS_PACKAGE_BOUNDARY:
throw new IllegalStateException(
String.format(
"Package boundary mode was cross: %s %s %s" + artifact, fileValue, request),
e);
case GENERATED_PATH_CONFLICT:
throw new IllegalStateException(
String.format(
"Generated conflict in source tree: %s %s %s", artifact, fileValue, request),
e);
}
throw new IllegalStateException("Can't get here", e);
}
if (value == null) {
return null;
}
Fingerprint fp = new Fingerprint();
for (ResolvedFile file : value.getTransitiveFiles().toList()) {
fp.addString(file.getNameInSymlinkTree().getPathString());
fp.addBytes(file.getMetadata().getDigest());
}
return FileArtifactValue.createForDirectoryWithHash(fp.digestAndReset());
}
@Nullable
private static RunfilesArtifactValue createRunfilesArtifactValue(
Artifact artifact,
ActionAnalysisMetadata action,
FileArtifactValue value,
SkyFunction.Environment env)
throws InterruptedException {
ImmutableList<Artifact> inputs = action.getInputs().toList();
SkyframeLookupResult values = env.getValuesAndExceptions(Artifact.keys(inputs));
if (env.valuesMissing()) {
return null;
}
ImmutableList.Builder<Artifact> files = ImmutableList.builder();
ImmutableList.Builder<FileArtifactValue> fileValues = ImmutableList.builder();
ImmutableList.Builder<Artifact> trees = ImmutableList.builder();
ImmutableList.Builder<TreeArtifactValue> treeValues = ImmutableList.builder();
// Sort for better equality in RunfilesArtifactValue.
ImmutableList<Artifact> sortedInputs =
ImmutableList.sortedCopyOf(Artifact.EXEC_PATH_COMPARATOR, inputs);
for (Artifact input : sortedInputs) {
SkyValue inputValue = values.get(Artifact.key(input));
if (inputValue == null) {
return null;
}
if (inputValue instanceof FileArtifactValue) {
files.add(input);
fileValues.add((FileArtifactValue) inputValue);
} else if (inputValue instanceof ActionExecutionValue) {
files.add(input);
fileValues.add(((ActionExecutionValue) inputValue).getExistingFileArtifactValue(input));
} else if (inputValue instanceof TreeArtifactValue) {
trees.add(input);
treeValues.add((TreeArtifactValue) inputValue);
} else {
// We do not recurse in middleman artifacts.
Preconditions.checkState(
!(inputValue instanceof RunfilesArtifactValue),
"%s %s %s",
artifact,
action,
inputValue);
}
}
return new RunfilesArtifactValue(
value, files.build(), fileValues.build(), trees.build(), treeValues.build());
}
/**
* Returns whether this value needs to contain the data of all its inputs. Currently, only tests
* to see if the action is a runfiles middleman action. However, may include Fileset artifacts in
* the future.
*/
private static boolean isAggregatingValue(ActionAnalysisMetadata action) {
return action.getActionType() == RUNFILES_MIDDLEMAN;
}
@Override
public String extractTag(SkyKey skyKey) {
return Label.print(((Artifact) skyKey).getOwner());
}
@Nullable
static ActionLookupValue getActionLookupValue(
ActionLookupKey actionLookupKey, SkyFunction.Environment env) throws InterruptedException {
ActionLookupValue value = (ActionLookupValue) env.getValue(actionLookupKey);
if (value == null) {
Preconditions.checkState(
actionLookupKey == CoverageReportValue.COVERAGE_REPORT_KEY,
"Not-yet-present artifact owner: %s",
actionLookupKey);
return null;
}
return value;
}
private static final class ArtifactFunctionException extends SkyFunctionException {
ArtifactFunctionException(ActionExecutionException e) {
super(e, Transience.TRANSIENT);
}
ArtifactFunctionException(SourceArtifactException e, Transience transience) {
super(e, transience);
}
}
private static String constructErrorMessage(Artifact artifact, String error) {
Label ownerLabel = artifact.getOwner();
if (ownerLabel == null || ownerLabel.getName().equals(".")) {
// Discovered inputs may not have an owner. Directory source artifacts may be owned by a label
// ':.' which will crash toPathFragment below.
return String.format("%s '%s'", error, artifact.getExecPathString());
}
PathFragment labelFragment = ownerLabel.toPathFragment();
if (ownerLabel.getRepository().isMain()) {
if (labelFragment.equals(artifact.getExecPath())) {
// No additional useful information from path.
return String.format("%s '%s'", error, ownerLabel);
}
} else {
// Not worth threading sibling repository layout config value all the way here: if either
// match, we know the label isn't useful.
for (boolean siblingRepositoryLayout : ImmutableList.of(Boolean.FALSE, Boolean.TRUE)) {
if (ownerLabel
.getRepository()
.getExecPath(siblingRepositoryLayout)
.getRelative(labelFragment)
.equals(artifact.getExecPath())) {
return String.format("%s '%s'", error, ownerLabel);
}
}
}
// TODO(bazel-team): when is this hit?
BugReport.sendBugReport(
new IllegalStateException("Unexpected special owner? " + artifact + ", " + ownerLabel));
return String.format("%s '%s', owner: '%s'", error, artifact.getExecPathString(), ownerLabel);
}
/** Describes dependencies of derived artifacts. */
// TODO(b/19539699): extend this to comprehensively support all special artifact types (e.g.
// middleman, etc).
public static final class ArtifactDependencies {
private final DerivedArtifact artifact;
private final ActionLookupValue actionLookupValue;
private ArtifactDependencies(DerivedArtifact artifact, ActionLookupValue actionLookupValue) {
this.artifact = artifact;
this.actionLookupValue = actionLookupValue;
}
/**
* Constructs an {@link ArtifactDependencies} for the provided {@code derivedArtifact}. Returns
* {@code null} if any dependencies are not yet ready.
*/
@Nullable
public static ArtifactDependencies discoverDependencies(
Artifact.DerivedArtifact derivedArtifact, SkyFunction.Environment env)
throws InterruptedException {
ActionLookupData generatingActionKey = derivedArtifact.getGeneratingActionKey();
ActionLookupValue actionLookupValue =
ArtifactFunction.getActionLookupValue(generatingActionKey.getActionLookupKey(), env);
if (actionLookupValue == null) {
return null;
}
return new ArtifactDependencies(derivedArtifact, actionLookupValue);
}
public boolean isTemplateActionForTreeArtifact() {
return maybeGetTemplateActionForTreeArtifact() != null;
}
@Nullable
ActionTemplate<?> maybeGetTemplateActionForTreeArtifact() {
if (!artifact.isTreeArtifact()) {
return null;
}
ActionAnalysisMetadata result =
actionLookupValue.getActions().get(artifact.getGeneratingActionKey().getActionIndex());
return result instanceof ActionTemplate ? (ActionTemplate<?>) result : null;
}
/**
* Returns action template expansion keys or {@code null} if that information is unavailable.
*
* <p>Must only be called if {@link #isTemplateActionForTreeArtifact} returns {@code true}.
*/
@Nullable
public ImmutableList<ActionLookupData> getActionTemplateExpansionKeys(
SkyFunction.Environment env) throws InterruptedException {
Preconditions.checkState(
isTemplateActionForTreeArtifact(), "Action is unexpectedly non-template: %s", this);
ActionTemplateExpansionKey key =
ActionTemplateExpansionValue.key(
artifact.getArtifactOwner(), artifact.getGeneratingActionKey().getActionIndex());
ActionTemplateExpansionValue value = (ActionTemplateExpansionValue) env.getValue(key);
if (value == null) {
return null;
}
ImmutableList.Builder<ActionLookupData> expandedActionExecutionKeys =
ImmutableList.builderWithExpectedSize(value.getNumActions());
for (ActionAnalysisMetadata action : value.getActions()) {
expandedActionExecutionKeys.add(
((DerivedArtifact) action.getPrimaryOutput()).getGeneratingActionKey());
}
return expandedActionExecutionKeys.build();
}
@Override
public String toString() {
return MoreObjects.toStringHelper(this)
.add("artifact", artifact)
.add("generatingActionKey", artifact.getGeneratingActionKey())
.add("actionLookupValue", actionLookupValue)
.toString();
}
}
/** An {@link Exception} thrown representing a source input {@link IOException}. */
public static final class SourceArtifactException extends Exception implements DetailedException {
private final DetailedExitCode detailedExitCode;
private SourceArtifactException(DetailedExitCode detailedExitCode, Exception e) {
super(detailedExitCode.getFailureDetail().getMessage(), e);
this.detailedExitCode = detailedExitCode;
}
private static SourceArtifactException create(Artifact artifact, IOException e) {
DetailedExitCode detailedExitCode =
DetailedExitCode.of(
FailureDetail.newBuilder()
.setMessage(
constructErrorMessage(artifact, "error reading file") + ": " + e.getMessage())
.setExecution(Execution.newBuilder().setCode(Code.SOURCE_INPUT_IO_EXCEPTION))
.build());
return new SourceArtifactException(detailedExitCode, e);
}
private static SourceArtifactException create(
Artifact artifact, RecursiveFilesystemTraversalException e) {
FailureDetail failureDetail =
FailureDetail.newBuilder()
.setMessage(
constructErrorMessage(artifact, "error traversing directory")
+ ": "
+ e.getMessage())
.setExecution(Execution.newBuilder().setCode(Code.SOURCE_INPUT_IO_EXCEPTION))
.build();
return new SourceArtifactException(DetailedExitCode.of(failureDetail), e);
}
@Override
public DetailedExitCode getDetailedExitCode() {
return detailedExitCode;
}
}
private static final class DirectoryArtifactTraversalRequest extends TraversalRequest {
private static final Interner<DirectoryArtifactTraversalRequest> interner =
BlazeInterners.newWeakInterner();
static DirectoryArtifactTraversalRequest create(
DirectTraversalRoot root, boolean skipTestingForSubpackage, Artifact artifact) {
return interner.intern(
new DirectoryArtifactTraversalRequest(root, skipTestingForSubpackage, artifact));
}
private final DirectTraversalRoot root;
private final boolean skipTestingForSubpackage;
private final Artifact artifact;
private DirectoryArtifactTraversalRequest(
DirectTraversalRoot root, boolean skipTestingForSubpackage, Artifact artifact) {
this.root = root;
this.skipTestingForSubpackage = skipTestingForSubpackage;
this.artifact = artifact;
}
@Override
public DirectTraversalRoot root() {
return root;
}
@Override
protected boolean isRootGenerated() {
return false;
}
@Override
protected PackageBoundaryMode crossPkgBoundaries() {
return PackageBoundaryMode.CROSS;
}
@Override
protected boolean strictOutputFiles() {
return true;
}
@Override
protected boolean skipTestingForSubpackage() {
return skipTestingForSubpackage;
}
@Override
protected boolean emitEmptyDirectoryNodes() {
return true;
}
@Override
protected String errorInfo() {
return "Directory artifact " + artifact.prettyPrint();
}
@Override
protected TraversalRequest duplicateWithOverrides(
DirectTraversalRoot newRoot, boolean newSkipTestingForSubpackage) {
return create(newRoot, newSkipTestingForSubpackage, artifact);
}
@Override
public int hashCode() {
// Artifact is only for error info and not considered in hash code or equality.
return root.hashCode() * 31 + Boolean.hashCode(skipTestingForSubpackage);
}
@Override
public boolean equals(Object o) {
if (this == o) {
return true;
}
if (!(o instanceof DirectoryArtifactTraversalRequest)) {
return false;
}
// Artifact is only for error info and not considered in hash code or equality.
DirectoryArtifactTraversalRequest other = (DirectoryArtifactTraversalRequest) o;
return root.equals(other.root) && skipTestingForSubpackage == other.skipTestingForSubpackage;
}
}
}