diff options
4 files changed, 489 insertions, 4 deletions
diff --git a/src/main/java/com/google/devtools/build/lib/exec/SpawnInputExpander.java b/src/main/java/com/google/devtools/build/lib/exec/SpawnInputExpander.java new file mode 100644 index 0000000000..2f69ab044a --- /dev/null +++ b/src/main/java/com/google/devtools/build/lib/exec/SpawnInputExpander.java @@ -0,0 +1,208 @@ +// Copyright 2017 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.exec; + +import static java.nio.charset.StandardCharsets.UTF_8; + +import com.google.common.annotations.VisibleForTesting; +import com.google.common.base.Preconditions; +import com.google.common.io.LineProcessor; +import com.google.devtools.build.lib.actions.ActionInput; +import com.google.devtools.build.lib.actions.ActionInputFileCache; +import com.google.devtools.build.lib.actions.ActionInputHelper; +import com.google.devtools.build.lib.actions.Artifact; +import com.google.devtools.build.lib.actions.Artifact.ArtifactExpander; +import com.google.devtools.build.lib.actions.RunfilesSupplier; +import com.google.devtools.build.lib.actions.Spawn; +import com.google.devtools.build.lib.analysis.AnalysisUtils; +import com.google.devtools.build.lib.rules.fileset.FilesetActionContext; +import com.google.devtools.build.lib.vfs.FileSystemUtils; +import com.google.devtools.build.lib.vfs.Path; +import com.google.devtools.build.lib.vfs.PathFragment; +import java.io.IOException; +import java.util.List; +import java.util.Map; +import java.util.Map.Entry; +import java.util.SortedMap; +import java.util.TreeMap; + +/** + * A helper class for spawn strategies to turn runfiles suppliers into input mappings. This class + * performs no I/O operations, but only rearranges the files according to how the runfiles should be + * laid out. + */ +public class SpawnInputExpander { + public static final ActionInput EMPTY_FILE = ActionInputHelper.fromPath("/dev/null"); + + private final boolean strict; + + /** + * Creates a new instance. If strict is true, then the expander checks for directories in runfiles + * and throws an exception if it finds any. Otherwise it silently ignores directories in runfiles + * and adds a mapping for them. At this time, directories in filesets are always silently added + * as mappings. + * + * <p>Directories in inputs are a correctness issue: Bazel only tracks dependencies at the action + * level, and it does not track dependencies on directories. Making a directory available to a + * spawn even though it's contents are not tracked as dependencies leads to incorrect incremental + * builds, since changes to the contents do not trigger action invalidation. + * + * <p>As such, all spawn strategies should always be strict and not make directories available to + * the subprocess. However, that's a breaking change, and therefore we make it depend on this flag + * for now. + */ + public SpawnInputExpander(boolean strict) { + this.strict = strict; + } + + private void addMapping( + Map<PathFragment, ActionInput> inputMappings, + PathFragment targetLocation, + ActionInput input) { + Preconditions.checkArgument(!targetLocation.isAbsolute(), targetLocation); + if (!inputMappings.containsKey(targetLocation)) { + inputMappings.put(targetLocation, input); + } + } + + /** Adds runfiles inputs from runfilesSupplier to inputMappings. */ + @VisibleForTesting + void addRunfilesToInputs( + Map<PathFragment, ActionInput> inputMap, + RunfilesSupplier runfilesSupplier, + ActionInputFileCache actionFileCache) throws IOException { + Map<PathFragment, Map<PathFragment, Artifact>> rootsAndMappings = null; + rootsAndMappings = runfilesSupplier.getMappings(); + + for (Entry<PathFragment, Map<PathFragment, Artifact>> rootAndMappings : + rootsAndMappings.entrySet()) { + PathFragment root = rootAndMappings.getKey(); + Preconditions.checkState(!root.isAbsolute(), root); + for (Entry<PathFragment, Artifact> mapping : rootAndMappings.getValue().entrySet()) { + PathFragment location = root.getRelative(mapping.getKey()); + Artifact localArtifact = mapping.getValue(); + if (localArtifact != null) { + if (strict && !actionFileCache.isFile(localArtifact)) { + throw new IOException("Not a file: " + localArtifact.getPath().getPathString()); + } + addMapping(inputMap, location, localArtifact); + } else { + addMapping(inputMap, location, EMPTY_FILE); + } + } + } + } + + /** + * Parses the fileset manifest file, adding to the inputMappings where + * appropriate. Lines referring to directories are recursed. + */ + @VisibleForTesting + void parseFilesetManifest( + Map<PathFragment, ActionInput> inputMappings, Artifact manifest, String workspaceName) + throws IOException { + Path file = manifest.getRoot().getPath().getRelative( + AnalysisUtils.getManifestPathFromFilesetPath(manifest.getExecPath()).getPathString()); + FileSystemUtils.asByteSource(file).asCharSource(UTF_8) + .readLines(new ManifestLineProcessor(inputMappings, workspaceName, manifest.getExecPath())); + } + + private final class ManifestLineProcessor implements LineProcessor<Object> { + private final Map<PathFragment, ActionInput> inputMap; + private final String workspaceName; + private final PathFragment targetPrefix; + private int lineNum = 0; + + ManifestLineProcessor( + Map<PathFragment, ActionInput> inputMap, + String workspaceName, + PathFragment targetPrefix) { + this.inputMap = inputMap; + this.workspaceName = workspaceName; + this.targetPrefix = targetPrefix; + } + + @Override + public boolean processLine(String line) throws IOException { + if (++lineNum % 2 == 0) { + // Digest line, skip. + return true; + } + if (line.isEmpty()) { + return true; + } + + ActionInput artifact; + PathFragment location; + int pos = line.indexOf(' '); + if (pos == -1) { + location = new PathFragment(line); + artifact = EMPTY_FILE; + } else { + String targetPath = line.substring(pos + 1); + if (targetPath.charAt(0) != '/') { + throw new IOException(String.format("runfiles target is not absolute: %s", targetPath)); + } + artifact = targetPath.isEmpty() ? EMPTY_FILE : ActionInputHelper.fromPath(targetPath); + + location = new PathFragment(line.substring(0, pos)); + if (!workspaceName.isEmpty()) { + if (!location.getSegment(0).equals(workspaceName)) { + throw new IOException( + String.format( + "fileset manifest line must start with '%s': '%s'", workspaceName, location)); + } else { + // Erase "<workspaceName>/". + location = location.subFragment(1, location.segmentCount()); + } + } + } + + addMapping(inputMap, targetPrefix.getRelative(location), artifact); + return true; + } + + @Override + public Object getResult() { + return null; // Unused. + } + } + + private void addInputs( + Map<PathFragment, ActionInput> inputMap, Spawn spawn, ArtifactExpander artifactExpander) { + List<ActionInput> inputs = + ActionInputHelper.expandArtifacts(spawn.getInputFiles(), artifactExpander); + for (ActionInput input : inputs) { + addMapping(inputMap, input.getExecPath(), input); + } + } + + /** + * Convert the inputs of the given spawn to a map from exec-root relative paths to action inputs. + * In some cases, this generates empty files, for which it uses {@link #EMPTY_FILE}. + */ + public SortedMap<PathFragment, ActionInput> getInputMapping( + Spawn spawn, ArtifactExpander artifactExpander, ActionInputFileCache actionInputFileCache, + FilesetActionContext filesetContext) + throws IOException { + TreeMap<PathFragment, ActionInput> inputMap = new TreeMap<>(); + addInputs(inputMap, spawn, artifactExpander); + addRunfilesToInputs( + inputMap, spawn.getRunfilesSupplier(), actionInputFileCache); + for (Artifact manifest : spawn.getFilesetManifests()) { + parseFilesetManifest(inputMap, manifest, filesetContext.getWorkspaceName()); + } + return inputMap; + } +} diff --git a/src/main/java/com/google/devtools/build/lib/sandbox/SandboxStrategy.java b/src/main/java/com/google/devtools/build/lib/sandbox/SandboxStrategy.java index 274377831b..c22be6662d 100644 --- a/src/main/java/com/google/devtools/build/lib/sandbox/SandboxStrategy.java +++ b/src/main/java/com/google/devtools/build/lib/sandbox/SandboxStrategy.java @@ -17,6 +17,8 @@ package com.google.devtools.build.lib.sandbox; import com.google.common.collect.ImmutableSet; import com.google.common.collect.ImmutableSet.Builder; import com.google.devtools.build.lib.actions.ActionExecutionContext; +import com.google.devtools.build.lib.actions.ActionInput; +import com.google.devtools.build.lib.actions.Artifact; import com.google.devtools.build.lib.actions.EnvironmentalExecException; import com.google.devtools.build.lib.actions.ExecException; import com.google.devtools.build.lib.actions.SandboxedSpawnActionContext; @@ -28,12 +30,17 @@ import com.google.devtools.build.lib.analysis.BlazeDirectories; import com.google.devtools.build.lib.buildtool.BuildRequest; import com.google.devtools.build.lib.events.Event; import com.google.devtools.build.lib.events.EventHandler; +import com.google.devtools.build.lib.exec.SpawnInputExpander; +import com.google.devtools.build.lib.rules.fileset.FilesetActionContext; import com.google.devtools.build.lib.util.io.OutErr; import com.google.devtools.build.lib.vfs.Path; import com.google.devtools.build.lib.vfs.PathFragment; import java.io.IOException; +import java.util.ArrayList; +import java.util.List; import java.util.Map; import java.util.Set; +import java.util.TreeMap; import java.util.concurrent.atomic.AtomicReference; /** Abstract common ancestor for sandbox strategies implementing the common parts. */ @@ -44,7 +51,7 @@ abstract class SandboxStrategy implements SandboxedSpawnActionContext { private final Path execRoot; private final boolean verboseFailures; private final SandboxOptions sandboxOptions; - private final SpawnHelpers spawnHelpers; + private final SpawnInputExpander spawnInputExpander; public SandboxStrategy( BuildRequest buildRequest, @@ -56,7 +63,7 @@ abstract class SandboxStrategy implements SandboxedSpawnActionContext { this.execRoot = blazeDirs.getExecRoot(); this.verboseFailures = verboseFailures; this.sandboxOptions = sandboxOptions; - this.spawnHelpers = new SpawnHelpers(blazeDirs.getExecRoot()); + this.spawnInputExpander = new SpawnInputExpander(/*strict=*/false); } protected void runSpawn( @@ -145,7 +152,34 @@ abstract class SandboxStrategy implements SandboxedSpawnActionContext { public Map<PathFragment, Path> getMounts(Spawn spawn, ActionExecutionContext executionContext) throws ExecException { try { - return spawnHelpers.getMounts(spawn, executionContext); + Map<PathFragment, ActionInput> inputMap = spawnInputExpander + .getInputMapping( + spawn, + executionContext.getArtifactExpander(), + executionContext.getActionInputFileCache(), + executionContext.getExecutor().getContext(FilesetActionContext.class)); + // SpawnInputExpander#getInputMapping uses ArtifactExpander#expandArtifacts to expand + // middlemen and tree artifacts, which expands empty tree artifacts to no entry. However, + // actions that accept TreeArtifacts as inputs generally expect that the empty directory is + // created. So we add those explicitly here. + // TODO(ulfjack): Move this code to SpawnInputExpander. + for (ActionInput input : spawn.getInputFiles()) { + if (input instanceof Artifact && ((Artifact) input).isTreeArtifact()) { + List<Artifact> containedArtifacts = new ArrayList<>(); + executionContext.getArtifactExpander().expand((Artifact) input, containedArtifacts); + // Attempting to mount a non-empty directory results in ERR_DIRECTORY_NOT_EMPTY, so we + // only mount empty TreeArtifacts as directories. + if (containedArtifacts.isEmpty()) { + inputMap.put(input.getExecPath(), input); + } + } + } + + Map<PathFragment, Path> mounts = new TreeMap<>(); + for (Map.Entry<PathFragment, ActionInput> e : inputMap.entrySet()) { + mounts.put(e.getKey(), execRoot.getRelative(e.getValue().getExecPath())); + } + return mounts; } catch (IOException e) { throw new EnvironmentalExecException("Could not prepare mounts for sandbox execution", e); } diff --git a/src/main/java/com/google/devtools/build/lib/sandbox/SymlinkedExecRoot.java b/src/main/java/com/google/devtools/build/lib/sandbox/SymlinkedExecRoot.java index 173f1cefa6..a158f02961 100644 --- a/src/main/java/com/google/devtools/build/lib/sandbox/SymlinkedExecRoot.java +++ b/src/main/java/com/google/devtools/build/lib/sandbox/SymlinkedExecRoot.java @@ -92,7 +92,8 @@ public final class SymlinkedExecRoot implements SandboxExecRoot { throws IOException { for (PathFragment inputPath : inputs) { Path dir = sandboxExecRoot.getRelative(inputPath).getParentDirectory(); - Preconditions.checkArgument(dir.startsWith(sandboxExecRoot)); + Preconditions.checkArgument( + dir.startsWith(sandboxExecRoot), "Bad relative path: '%s'", inputPath); FileSystemUtils.createDirectoryAndParentsWithCache(createdDirs, dir); } } diff --git a/src/test/java/com/google/devtools/build/lib/exec/SpawnInputExpanderTest.java b/src/test/java/com/google/devtools/build/lib/exec/SpawnInputExpanderTest.java new file mode 100644 index 0000000000..fdfa655371 --- /dev/null +++ b/src/test/java/com/google/devtools/build/lib/exec/SpawnInputExpanderTest.java @@ -0,0 +1,242 @@ +// Copyright 2017 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.exec; + +import static com.google.common.truth.Truth.assertThat; +import static org.junit.Assert.fail; + +import com.google.common.collect.Maps; +import com.google.devtools.build.lib.actions.ActionInput; +import com.google.devtools.build.lib.actions.ActionInputFileCache; +import com.google.devtools.build.lib.actions.ActionInputHelper; +import com.google.devtools.build.lib.actions.Artifact; +import com.google.devtools.build.lib.actions.EmptyRunfilesSupplier; +import com.google.devtools.build.lib.actions.Root; +import com.google.devtools.build.lib.actions.RunfilesSupplier; +import com.google.devtools.build.lib.analysis.Runfiles; +import com.google.devtools.build.lib.analysis.RunfilesSupplierImpl; +import com.google.devtools.build.lib.vfs.FileSystem; +import com.google.devtools.build.lib.vfs.FileSystemUtils; +import com.google.devtools.build.lib.vfs.Path; +import com.google.devtools.build.lib.vfs.PathFragment; +import com.google.devtools.build.lib.vfs.inmemoryfs.InMemoryFileSystem; +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.util.Map; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.JUnit4; +import org.mockito.Mockito; + +/** + * Tests for {@link SpawnInputExpander}. + */ +@RunWith(JUnit4.class) +public class SpawnInputExpanderTest { + private FileSystem fs; + private SpawnInputExpander expander; + private Map<PathFragment, ActionInput> inputMappings; + + @Before + public final void createSpawnInputExpander() throws Exception { + fs = new InMemoryFileSystem(); + expander = new SpawnInputExpander(/*strict=*/true); + inputMappings = Maps.newHashMap(); + } + + private void scratchFile(String file, String... lines) throws Exception { + Path path = fs.getPath(file); + FileSystemUtils.createDirectoryAndParents(path.getParentDirectory()); + FileSystemUtils.writeLinesAs(path, StandardCharsets.UTF_8, lines); + } + + @Test + public void testEmptyRunfiles() throws Exception { + RunfilesSupplier supplier = EmptyRunfilesSupplier.INSTANCE; + expander.addRunfilesToInputs(inputMappings, supplier, null); + assertThat(inputMappings).isEmpty(); + } + + @Test + public void testRunfilesSingleFile() throws Exception { + Artifact artifact = + new Artifact(fs.getPath("/root/dir/file"), Root.asSourceRoot(fs.getPath("/root"))); + Runfiles runfiles = new Runfiles.Builder("workspace").addArtifact(artifact).build(); + RunfilesSupplier supplier = new RunfilesSupplierImpl(new PathFragment("runfiles"), runfiles); + ActionInputFileCache mockCache = Mockito.mock(ActionInputFileCache.class); + Mockito.when(mockCache.isFile(artifact)).thenReturn(true); + + expander.addRunfilesToInputs(inputMappings, supplier, mockCache); + assertThat(inputMappings).hasSize(1); + assertThat(inputMappings) + .containsEntry(new PathFragment("runfiles/workspace/dir/file"), artifact); + } + + @Test + public void testRunfilesDirectoryStrict() throws Exception { + Artifact artifact = + new Artifact(fs.getPath("/root/dir/file"), Root.asSourceRoot(fs.getPath("/root"))); + Runfiles runfiles = new Runfiles.Builder("workspace").addArtifact(artifact).build(); + RunfilesSupplier supplier = new RunfilesSupplierImpl(new PathFragment("runfiles"), runfiles); + ActionInputFileCache mockCache = Mockito.mock(ActionInputFileCache.class); + Mockito.when(mockCache.isFile(artifact)).thenReturn(false); + + try { + expander.addRunfilesToInputs(inputMappings, supplier, mockCache); + fail(); + } catch (IOException expected) { + assertThat(expected.getMessage().contains("Not a file: /root/dir/file")).isTrue(); + } + } + + @Test + public void testRunfilesDirectoryNonStrict() throws Exception { + Artifact artifact = + new Artifact(fs.getPath("/root/dir/file"), Root.asSourceRoot(fs.getPath("/root"))); + Runfiles runfiles = new Runfiles.Builder("workspace").addArtifact(artifact).build(); + RunfilesSupplier supplier = new RunfilesSupplierImpl(new PathFragment("runfiles"), runfiles); + ActionInputFileCache mockCache = Mockito.mock(ActionInputFileCache.class); + Mockito.when(mockCache.isFile(artifact)).thenReturn(false); + + expander = new SpawnInputExpander(/*strict=*/false); + expander.addRunfilesToInputs(inputMappings, supplier, mockCache); + assertThat(inputMappings).hasSize(1); + assertThat(inputMappings) + .containsEntry(new PathFragment("runfiles/workspace/dir/file"), artifact); + } + + @Test + public void testRunfilesTwoFiles() throws Exception { + Artifact artifact1 = + new Artifact(fs.getPath("/root/dir/file"), Root.asSourceRoot(fs.getPath("/root"))); + Artifact artifact2 = + new Artifact(fs.getPath("/root/dir/baz"), Root.asSourceRoot(fs.getPath("/root"))); + Runfiles runfiles = new Runfiles.Builder("workspace") + .addArtifact(artifact1) + .addArtifact(artifact2) + .build(); + RunfilesSupplier supplier = new RunfilesSupplierImpl(new PathFragment("runfiles"), runfiles); + ActionInputFileCache mockCache = Mockito.mock(ActionInputFileCache.class); + Mockito.when(mockCache.isFile(artifact1)).thenReturn(true); + Mockito.when(mockCache.isFile(artifact2)).thenReturn(true); + + expander.addRunfilesToInputs(inputMappings, supplier, mockCache); + assertThat(inputMappings).hasSize(2); + assertThat(inputMappings) + .containsEntry(new PathFragment("runfiles/workspace/dir/file"), artifact1); + assertThat(inputMappings) + .containsEntry(new PathFragment("runfiles/workspace/dir/baz"), artifact2); + } + + @Test + public void testRunfilesSymlink() throws Exception { + Artifact artifact = + new Artifact(fs.getPath("/root/dir/file"), Root.asSourceRoot(fs.getPath("/root"))); + Runfiles runfiles = new Runfiles.Builder("workspace") + .addSymlink(new PathFragment("symlink"), artifact).build(); + RunfilesSupplier supplier = new RunfilesSupplierImpl(new PathFragment("runfiles"), runfiles); + ActionInputFileCache mockCache = Mockito.mock(ActionInputFileCache.class); + Mockito.when(mockCache.isFile(artifact)).thenReturn(true); + + expander.addRunfilesToInputs(inputMappings, supplier, mockCache); + assertThat(inputMappings).hasSize(1); + assertThat(inputMappings) + .containsEntry(new PathFragment("runfiles/workspace/symlink"), artifact); + } + + @Test + public void testRunfilesRootSymlink() throws Exception { + Artifact artifact = + new Artifact(fs.getPath("/root/dir/file"), Root.asSourceRoot(fs.getPath("/root"))); + Runfiles runfiles = new Runfiles.Builder("workspace") + .addRootSymlink(new PathFragment("symlink"), artifact).build(); + RunfilesSupplier supplier = new RunfilesSupplierImpl(new PathFragment("runfiles"), runfiles); + ActionInputFileCache mockCache = Mockito.mock(ActionInputFileCache.class); + Mockito.when(mockCache.isFile(artifact)).thenReturn(true); + + expander.addRunfilesToInputs(inputMappings, supplier, mockCache); + assertThat(inputMappings).hasSize(2); + assertThat(inputMappings).containsEntry(new PathFragment("runfiles/symlink"), artifact); + // If there's no other entry, Runfiles adds an empty file in the workspace to make sure the + // directory gets created. + assertThat(inputMappings) + .containsEntry( + new PathFragment("runfiles/workspace/.runfile"), SpawnInputExpander.EMPTY_FILE); + } + + @Test + public void testEmptyManifest() throws Exception { + // See AnalysisUtils for the mapping from "foo" to "_foo/MANIFEST". + scratchFile("/root/_foo/MANIFEST"); + + Artifact artifact = + new Artifact(fs.getPath("/root/foo"), Root.asSourceRoot(fs.getPath("/root"))); + expander.parseFilesetManifest(inputMappings, artifact, "workspace"); + assertThat(inputMappings).isEmpty(); + } + + @Test + public void testManifestWithSingleFile() throws Exception { + // See AnalysisUtils for the mapping from "foo" to "_foo/MANIFEST". + scratchFile( + "/root/_foo/MANIFEST", + "workspace/bar /dir/file", + "<some digest>"); + + Artifact artifact = + new Artifact(fs.getPath("/root/foo"), Root.asSourceRoot(fs.getPath("/root"))); + expander.parseFilesetManifest(inputMappings, artifact, "workspace"); + assertThat(inputMappings).hasSize(1); + assertThat(inputMappings) + .containsEntry(new PathFragment("foo/bar"), ActionInputHelper.fromPath("/dir/file")); + } + + @Test + public void testManifestWithTwoFiles() throws Exception { + // See AnalysisUtils for the mapping from "foo" to "_foo/MANIFEST". + scratchFile( + "/root/_foo/MANIFEST", + "workspace/bar /dir/file", + "<some digest>", + "workspace/baz /dir/file", + "<some digest>"); + + Artifact artifact = + new Artifact(fs.getPath("/root/foo"), Root.asSourceRoot(fs.getPath("/root"))); + expander.parseFilesetManifest(inputMappings, artifact, "workspace"); + assertThat(inputMappings).hasSize(2); + assertThat(inputMappings) + .containsEntry(new PathFragment("foo/bar"), ActionInputHelper.fromPath("/dir/file")); + assertThat(inputMappings) + .containsEntry(new PathFragment("foo/baz"), ActionInputHelper.fromPath("/dir/file")); + } + + @Test + public void testManifestWithDirectory() throws Exception { + // See AnalysisUtils for the mapping from "foo" to "_foo/MANIFEST". + scratchFile( + "/root/_foo/MANIFEST", + "workspace/bar /some", + "<some digest>"); + + Artifact artifact = + new Artifact(fs.getPath("/root/foo"), Root.asSourceRoot(fs.getPath("/root"))); + expander.parseFilesetManifest(inputMappings, artifact, "workspace"); + assertThat(inputMappings).hasSize(1); + assertThat(inputMappings) + .containsEntry( + new PathFragment("foo/bar"), ActionInputHelper.fromPath("/some")); + } +} |