private ResolvedTargets<Void> getTargetsInPackage( String originalPattern, PathFragment packageNameFragment, FilteringPolicy policy) throws TargetParsingException, InterruptedException { TargetPatternResolverUtil.validatePatternPackage(originalPattern, packageNameFragment, this); try { PackageIdentifier packageId = PackageIdentifier.createInDefaultRepo(packageNameFragment); Package pkg = packageProvider.getPackage(env.getListener(), packageId); ResolvedTargets<Target> packageTargets = TargetPatternResolverUtil.resolvePackageTargets(pkg, policy); ImmutableList.Builder<SkyKey> builder = ImmutableList.builder(); for (Target target : packageTargets.getTargets()) { builder.add(TransitiveTraversalValue.key(target.getLabel())); } ImmutableList<SkyKey> skyKeys = builder.build(); env.getValuesOrThrow(skyKeys, NoSuchPackageException.class, NoSuchTargetException.class); if (env.valuesMissing()) { throw new MissingDepException(); } return ResolvedTargets.empty(); } catch (NoSuchThingException e) { String message = TargetPatternResolverUtil.getParsingErrorMessage( "package contains errors", originalPattern); throw new TargetParsingException(message, e); } }
@Override public boolean isPackage(String packageName) { // TODO(bazel-team): this should get the whole PackageIdentifier. Using only the package name // makes it impossible to use wildcards to refer to targets in remote repositories. return packageProvider.isPackage( env.getListener(), PackageIdentifier.createInDefaultRepo(packageName)); }
private static SkyKey key(RepositoryName repo, PathFragment fileToImport) throws ASTLookupInputException { // Skylark import lookup keys need to be valid AST file lookup keys. checkInputArgument(fileToImport); return new SkyKey( SkyFunctions.SKYLARK_IMPORTS_LOOKUP, PackageIdentifier.create(repo, fileToImport)); }
private void assertSkipsFoo(ImmutableList<String> patternSequence) throws Exception { // When PrepareDepsOfPatternsFunction completes evaluation (successfully), WalkableGraph walkableGraph = getGraphFromPatternsEvaluation( patternSequence, /*successExpected=*/ true, /*keepGoing=*/ true); // Then the graph contains a package value for "@//foo", assertTrue(walkableGraph.exists(PackageValue.key(PackageIdentifier.parse("@//foo")))); // But no package value for "@//foo/foo", assertFalse(walkableGraph.exists(PackageValue.key(PackageIdentifier.parse("@//foo/foo")))); // And the graph does not contain a value for the target "@//foo/foo:foofoo". Label label = Label.create("@//foo/foo", "foofoo"); assertFalse(walkableGraph.exists(getKeyForLabel(label))); }
@Test public void testRecursiveEvaluationFailsOnBadBuildFile() throws Exception { // Given a well-formed package "@//foo" and a malformed package "@//foo/foo", createFooAndFooFoo(); // Given a target pattern sequence consisting of a recursive pattern for "//foo/...", ImmutableList<String> patternSequence = ImmutableList.of("//foo/..."); // When PrepareDepsOfPatternsFunction completes evaluation (with no error because it was // recovered from), WalkableGraph walkableGraph = getGraphFromPatternsEvaluation( patternSequence, /*successExpected=*/ true, /*keepGoing=*/ true); // Then the graph contains package values for "@//foo" and "@//foo/foo", assertTrue(walkableGraph.exists(PackageValue.key(PackageIdentifier.parse("@//foo")))); assertTrue(walkableGraph.exists(PackageValue.key(PackageIdentifier.parse("@//foo/foo")))); // But the graph does not contain a value for the target "@//foo/foo:foofoo". assertFalse(walkableGraph.exists(getKeyForLabel(Label.create("@//foo/foo", "foofoo")))); }
@Before public void setUp() throws Exception { buildFile = scratch.file("isolated/BUILD", "# contents don't matter in this test"); scratch.file("isolated/sub/BUILD", "# contents don't matter in this test"); packageDirectory = buildFile.getParentDirectory(); scratch.file("isolated/first.txt", "# this is first.txt"); scratch.file("isolated/second.txt", "# this is second.txt"); scratch.file("isolated/first.js", "# this is first.js"); scratch.file("isolated/second.js", "# this is second.js"); // Files in subdirectories scratch.file("isolated/foo/first.js", "# this is foo/first.js"); scratch.file("isolated/foo/second.js", "# this is foo/second.js"); scratch.file("isolated/bar/first.js", "# this is bar/first.js"); scratch.file("isolated/bar/second.js", "# this is bar/second.js"); scratch.file("isolated/sub/sub.js", "# this is sub/sub.js"); cache = new GlobCache( packageDirectory, PackageIdentifier.createInDefaultRepo("isolated"), new CachingPackageLocator() { @Override public Path getBuildFileForPackage(PackageIdentifier packageId) { String packageName = packageId.getPackageFragment().getPathString(); if (packageName.equals("isolated")) { return scratch.resolve("isolated/BUILD"); } else if (packageName.equals("isolated/sub")) { return scratch.resolve("isolated/sub/BUILD"); } else { return null; } } }, null, TestUtils.getPool()); }
private Package deserializeInternal(InputStream in) throws PackageDeserializationException, IOException, InterruptedException { // Read the initial Package message so we have the data to initialize the builder. We will read // the Targets in individually later. Build.Package packagePb = Build.Package.parseDelimitedFrom(in); Package.Builder builder; try { builder = new Package.Builder( PackageIdentifier.create( packagePb.getRepository(), new PathFragment(packagePb.getName())), null); } catch (LabelSyntaxException e) { throw new PackageDeserializationException(e); } StoredEventHandler eventHandler = new StoredEventHandler(); deserializeInternal(packagePb, eventHandler, builder, in); builder.addEvents(eventHandler.getEvents()); return builder.build(); }
/** * Converts the PathFragment of the Skylark file to a Label using the BUILD file closest to the * Skylark file in its directory hierarchy - finds the package to which the Skylark file belongs. * Throws an exception if no such BUILD file exists. */ private Label pathFragmentToLabel(RepositoryName repo, PathFragment file, Environment env) throws SkylarkImportLookupFunctionException { ContainingPackageLookupValue containingPackageLookupValue = null; try { PackageIdentifier newPkgId = PackageIdentifier.create(repo, file.getParentDirectory()); containingPackageLookupValue = (ContainingPackageLookupValue) env.getValueOrThrow( ContainingPackageLookupValue.key(newPkgId), BuildFileNotFoundException.class, InconsistentFilesystemException.class); } catch (BuildFileNotFoundException e) { // Thrown when there are IO errors looking for BUILD files. throw new SkylarkImportLookupFunctionException(e, Transience.PERSISTENT); } catch (InconsistentFilesystemException e) { throw new SkylarkImportLookupFunctionException(e, Transience.PERSISTENT); } if (containingPackageLookupValue == null) { return null; } if (!containingPackageLookupValue.hasContainingPackage()) { throw new SkylarkImportLookupFunctionException( SkylarkImportFailedException.noBuildFile(file)); } PathFragment pkgName = containingPackageLookupValue.getContainingPackageName().getPackageFragment(); PathFragment fileInPkg = file.relativeTo(pkgName); try { // This code relies on PackageIdentifier.RepositoryName.toString() return Label.parseAbsolute(repo + "//" + pkgName.getPathString() + ":" + fileInPkg); } catch (LabelSyntaxException e) { throw new IllegalStateException(e); } }
/** * Get SkyKeys for the FileValues for the given {@code pathFragments}. To do this, we look for a * package lookup node for each path fragment, since package lookup nodes contain the "root" of a * package. The returned SkyKeys correspond to FileValues that may not exist in the graph. */ private Collection<SkyKey> getSkyKeysForFileFragments(Iterable<PathFragment> pathFragments) { Set<SkyKey> result = new HashSet<>(); Multimap<PathFragment, PathFragment> currentToOriginal = ArrayListMultimap.create(); for (PathFragment pathFragment : pathFragments) { currentToOriginal.put(pathFragment, pathFragment); } while (!currentToOriginal.isEmpty()) { Map<SkyKey, PathFragment> keys = new HashMap<>(); for (PathFragment pathFragment : currentToOriginal.keySet()) { keys.put( PackageLookupValue.key(PackageIdentifier.createInDefaultRepo(pathFragment)), pathFragment); } Map<SkyKey, SkyValue> lookupValues = graph.getSuccessfulValues(keys.keySet()); for (Map.Entry<SkyKey, SkyValue> entry : lookupValues.entrySet()) { PackageLookupValue packageLookupValue = (PackageLookupValue) entry.getValue(); if (packageLookupValue.packageExists()) { PathFragment dir = keys.get(entry.getKey()); Collection<PathFragment> originalFiles = currentToOriginal.get(dir); Preconditions.checkState(!originalFiles.isEmpty(), entry); for (PathFragment fileName : originalFiles) { result.add( FileValue.key(RootedPath.toRootedPath(packageLookupValue.getRoot(), fileName))); } currentToOriginal.removeAll(dir); } } Multimap<PathFragment, PathFragment> newCurrentToOriginal = ArrayListMultimap.create(); for (PathFragment pathFragment : currentToOriginal.keySet()) { PathFragment parent = pathFragment.getParentDirectory(); if (parent != null) { newCurrentToOriginal.putAll(parent, currentToOriginal.get(pathFragment)); } } currentToOriginal = newCurrentToOriginal; } return result; }
SkyValue computeInternal( SkyKey skyKey, Environment env, @Nullable Set<SkyKey> visitedKeysForCycle) throws SkyFunctionException, InterruptedException { PackageIdentifier arg = (PackageIdentifier) skyKey.argument(); PathFragment file = arg.getPackageFragment(); ASTFileLookupValue astLookupValue = null; try { SkyKey astLookupKey = ASTFileLookupValue.key(arg); astLookupValue = (ASTFileLookupValue) env.getValueOrThrow( astLookupKey, ErrorReadingSkylarkExtensionException.class, InconsistentFilesystemException.class); } catch (ErrorReadingSkylarkExtensionException e) { throw new SkylarkImportLookupFunctionException( SkylarkImportFailedException.errorReadingFile(file, e.getMessage())); } catch (InconsistentFilesystemException e) { throw new SkylarkImportLookupFunctionException(e, Transience.PERSISTENT); } if (astLookupValue == null) { return null; } if (astLookupValue.getAST() == null) { // Skylark import files have to exist. throw new SkylarkImportLookupFunctionException(SkylarkImportFailedException.noFile(file)); } BuildFileAST ast = astLookupValue.getAST(); if (ast.containsErrors()) { throw new SkylarkImportLookupFunctionException( SkylarkImportFailedException.skylarkErrors(file)); } Label label = pathFragmentToLabel(arg.getRepository(), file, env); if (label == null) { Preconditions.checkState(env.valuesMissing(), "null label with no missing %s", file); return null; } Map<Location, PathFragment> astImports = ast.getImports(); Map<PathFragment, Extension> importMap = Maps.newHashMapWithExpectedSize(astImports.size()); ImmutableList.Builder<SkylarkFileDependency> fileDependencies = ImmutableList.builder(); Map<SkyKey, PathFragment> skylarkImports = Maps.newHashMapWithExpectedSize(astImports.size()); for (Map.Entry<Location, PathFragment> entry : ast.getImports().entrySet()) { try { skylarkImports.put( PackageFunction.getImportKey(entry, ruleClassProvider.getPreludePath(), file, arg), entry.getValue()); } catch (ASTLookupInputException e) { throw new SkylarkImportLookupFunctionException(e, Transience.PERSISTENT); } } Map<SkyKey, SkyValue> skylarkImportMap; boolean valuesMissing = false; if (visitedKeysForCycle == null) { // Not inlining. skylarkImportMap = env.getValues(skylarkImports.keySet()); valuesMissing = env.valuesMissing(); } else { // inlining calls to SkylarkImportLookupFunction. if (!visitedKeysForCycle.add(skyKey)) { ImmutableList<SkyKey> cycle = CycleUtils.splitIntoPathAndChain(Predicates.equalTo(skyKey), visitedKeysForCycle) .second; if (env.getValue(SkylarkImportUniqueCycleValue.key(cycle)) == null) { return null; } throw new SkylarkImportLookupFunctionException( new SkylarkImportFailedException("Skylark import cycle")); } skylarkImportMap = Maps.newHashMapWithExpectedSize(astImports.size()); for (SkyKey skylarkImport : skylarkImports.keySet()) { SkyValue skyValue = this.computeWithInlineCalls(skylarkImport, env, visitedKeysForCycle); if (skyValue == null) { Preconditions.checkState( env.valuesMissing(), "no skylark import value for %s", skylarkImport); // Don't give up on computing. This is against the Skyframe contract, but we don't want to // pay the price of serializing all these calls, since they are fundamentally independent. valuesMissing = true; } else { skylarkImportMap.put(skylarkImport, skyValue); } } // All imports traversed, this key can no longer be part of a cycle. visitedKeysForCycle.remove(skyKey); } if (valuesMissing) { // This means some imports are unavailable. return null; } for (Map.Entry<SkyKey, SkyValue> entry : skylarkImportMap.entrySet()) { SkylarkImportLookupValue importLookupValue = (SkylarkImportLookupValue) entry.getValue(); importMap.put( skylarkImports.get(entry.getKey()), importLookupValue.getEnvironmentExtension()); fileDependencies.add(importLookupValue.getDependency()); } // Skylark UserDefinedFunction-s in that file will share this function definition Environment, // which will be frozen by the time it is returned by createExtension. Extension extension = createExtension(ast, file, importMap, env); return new SkylarkImportLookupValue( extension, new SkylarkFileDependency(label, fileDependencies.build())); }
@VisibleForTesting static SkyKey key(PackageIdentifier pkgIdentifier) throws ASTLookupInputException { return key(pkgIdentifier.getRepository(), pkgIdentifier.getPackageFragment()); }
@Override public SkyValue compute(SkyKey skyKey, Environment env) throws SkyFunctionException, InterruptedException { PackageIdentifier arg = (PackageIdentifier) skyKey.argument(); PathFragment file = arg.getPackageFragment(); ASTFileLookupValue astLookupValue = null; try { SkyKey astLookupKey = ASTFileLookupValue.key(arg); astLookupValue = (ASTFileLookupValue) env.getValueOrThrow( astLookupKey, ErrorReadingSkylarkExtensionException.class, InconsistentFilesystemException.class); } catch (ErrorReadingSkylarkExtensionException e) { throw new SkylarkImportLookupFunctionException( SkylarkImportFailedException.errorReadingFile(file, e.getMessage())); } catch (InconsistentFilesystemException e) { throw new SkylarkImportLookupFunctionException(e, Transience.PERSISTENT); } if (astLookupValue == null) { return null; } if (astLookupValue.getAST() == null) { // Skylark import files have to exist. throw new SkylarkImportLookupFunctionException(SkylarkImportFailedException.noFile(file)); } BuildFileAST ast = astLookupValue.getAST(); if (ast.containsErrors()) { throw new SkylarkImportLookupFunctionException( SkylarkImportFailedException.skylarkErrors(file)); } Map<Location, PathFragment> astImports = ast.getImports(); Map<PathFragment, Extension> importMap = Maps.newHashMapWithExpectedSize(astImports.size()); ImmutableList.Builder<SkylarkFileDependency> fileDependencies = ImmutableList.builder(); Map<SkyKey, PathFragment> skylarkImports = Maps.newHashMapWithExpectedSize(astImports.size()); for (Map.Entry<Location, PathFragment> entry : ast.getImports().entrySet()) { try { skylarkImports.put( PackageFunction.getImportKey(entry, ruleClassProvider.getPreludePath(), file, arg), entry.getValue()); } catch (ASTLookupInputException e) { throw new SkylarkImportLookupFunctionException(e, Transience.PERSISTENT); } } Map<SkyKey, SkyValue> skylarkImportMap = env.getValues(skylarkImports.keySet()); if (env.valuesMissing()) { // This means some imports are unavailable. return null; } for (Map.Entry<SkyKey, SkyValue> entry : skylarkImportMap.entrySet()) { SkylarkImportLookupValue importLookupValue = (SkylarkImportLookupValue) entry.getValue(); importMap.put( skylarkImports.get(entry.getKey()), importLookupValue.getEnvironmentExtension()); fileDependencies.add(importLookupValue.getDependency()); } Label label = pathFragmentToLabel(arg.getRepository(), file, env); if (label == null) { Preconditions.checkState(env.valuesMissing(), "label null but no missing for %s", file); return null; } // Skylark UserDefinedFunction-s in that file will share this function definition Environment, // which will be frozen by the time it is returned by createExtension. Extension extension = createExtension(ast, file, importMap, env); return new SkylarkImportLookupValue( extension, new SkylarkFileDependency(label, fileDependencies.build())); }
public BuildFileContainsErrorsException(PackageIdentifier packageIdentifier) { super( packageIdentifier, "Package '" + packageIdentifier.getPackageFragment().getPathString() + "' contains errors"); }
/** * Looks in the directory specified by {@code recursivePkgKey} for a package, does some work as * specified by {@link Visitor} if such a package exists, then recursively does work in each * non-excluded subdirectory as specified by {@link #getSkyKeyForSubdirectory}, and finally * aggregates the {@link Visitor} value along with values from each subdirectory as specified by * {@link #aggregateWithSubdirectorySkyValues}, and returns that aggregation. * * <p>Returns null if {@code env.valuesMissing()} is true, checked after each call to one of * {@link RecursiveDirectoryTraversalFunction}'s abstract methods that were given {@code env}. * (And after each of {@code visitDirectory}'s own uses of {@code env}, of course.) */ TReturn visitDirectory(RecursivePkgKey recursivePkgKey, Environment env) { RootedPath rootedPath = recursivePkgKey.getRootedPath(); ImmutableSet<PathFragment> excludedPaths = recursivePkgKey.getExcludedPaths(); Path root = rootedPath.getRoot(); PathFragment rootRelativePath = rootedPath.getRelativePath(); SkyKey fileKey = FileValue.key(rootedPath); FileValue fileValue; try { fileValue = (FileValue) env.getValueOrThrow( fileKey, InconsistentFilesystemException.class, FileSymlinkException.class, IOException.class); } catch (InconsistentFilesystemException | FileSymlinkException | IOException e) { return reportErrorAndReturn( "Failed to get information about path", e, rootRelativePath, env.getListener()); } if (fileValue == null) { return null; } if (!fileValue.isDirectory()) { return getEmptyReturn(); } PackageIdentifier packageId = PackageIdentifier.create(recursivePkgKey.getRepository(), rootRelativePath); if ((packageId.getRepository().isDefault() || packageId.getRepository().isMain()) && fileValue.isSymlink() && fileValue .getUnresolvedLinkTarget() .startsWith(directories.getOutputBase().asFragment())) { // Symlinks back to the output base are not traversed so that we avoid convenience symlinks. // Note that it's not enough to just check for the convenience symlinks themselves, because // if the value of --symlink_prefix changes, the old symlinks are left in place. This // algorithm also covers more creative use cases where people create convenience symlinks // somewhere in the directory tree manually. return getEmptyReturn(); } SkyKey pkgLookupKey = PackageLookupValue.key(packageId); SkyKey dirListingKey = DirectoryListingValue.key(rootedPath); Map< SkyKey, ValueOrException4< NoSuchPackageException, InconsistentFilesystemException, FileSymlinkException, IOException>> pkgLookupAndDirectoryListingDeps = env.getValuesOrThrow( ImmutableList.of(pkgLookupKey, dirListingKey), NoSuchPackageException.class, InconsistentFilesystemException.class, FileSymlinkException.class, IOException.class); if (env.valuesMissing()) { return null; } PackageLookupValue pkgLookupValue; try { pkgLookupValue = (PackageLookupValue) Preconditions.checkNotNull( pkgLookupAndDirectoryListingDeps.get(pkgLookupKey).get(), "%s %s", recursivePkgKey, pkgLookupKey); } catch (NoSuchPackageException | InconsistentFilesystemException e) { return reportErrorAndReturn("Failed to load package", e, rootRelativePath, env.getListener()); } catch (IOException | FileSymlinkException e) { throw new IllegalStateException(e); } TVisitor visitor = getInitialVisitor(); DirectoryListingValue dirListingValue; try { dirListingValue = (DirectoryListingValue) Preconditions.checkNotNull( pkgLookupAndDirectoryListingDeps.get(dirListingKey).get(), "%s %s", recursivePkgKey, dirListingKey); } catch (InconsistentFilesystemException | IOException e) { return reportErrorAndReturn( "Failed to list directory contents", e, rootRelativePath, env.getListener()); } catch (FileSymlinkException e) { // DirectoryListingFunction only throws FileSymlinkCycleException when FileFunction throws it, // but FileFunction was evaluated for rootedPath above, and didn't throw there. It shouldn't // be able to avoid throwing there but throw here. throw new IllegalStateException( "Symlink cycle found after not being found for \"" + rootedPath + "\""); } catch (NoSuchPackageException e) { throw new IllegalStateException(e); } boolean followSymlinks = shouldFollowSymlinksWhenTraversing(dirListingValue.getDirents()); List<SkyKey> childDeps = new ArrayList<>(); for (Dirent dirent : dirListingValue.getDirents()) { Type type = dirent.getType(); if (type != Type.DIRECTORY && (type != Type.SYMLINK || (type == Type.SYMLINK && !followSymlinks))) { // Non-directories can never host packages. Symlinks to non-directories are weeded out at // the next level of recursion when we check if its FileValue is a directory. This is slower // if there are a lot of symlinks in the tree, but faster if there are only a few, which is // the case most of the time. // // We are not afraid of weird symlink structure here: both cyclical ones and ones that give // rise to infinite directory trees are diagnosed by FileValue. continue; } String basename = dirent.getName(); if (rootRelativePath.equals(PathFragment.EMPTY_FRAGMENT) && PathPackageLocator.DEFAULT_TOP_LEVEL_EXCLUDES.contains(basename)) { continue; } PathFragment subdirectory = rootRelativePath.getRelative(basename); // If this subdirectory is one of the excluded paths, don't recurse into it. if (excludedPaths.contains(subdirectory)) { continue; } // If we have an excluded path that isn't below this subdirectory, we shouldn't pass that // excluded path to our evaluation of the subdirectory, because the exclusion can't // possibly match anything beneath the subdirectory. // // For example, if we're currently evaluating directory "a", are looking at its subdirectory // "a/b", and we have an excluded path "a/c/d", there's no need to pass the excluded path // "a/c/d" to our evaluation of "a/b". // // This strategy should help to get more skyframe sharing. Consider the example above. A // subsequent request of "a/b/...", without any excluded paths, will be a cache hit. // // TODO(bazel-team): Replace the excludedPaths set with a trie or a SortedSet for better // efficiency. ImmutableSet<PathFragment> excludedSubdirectoriesBeneathThisSubdirectory = PathFragment.filterPathsStartingWith(excludedPaths, subdirectory); RootedPath subdirectoryRootedPath = RootedPath.toRootedPath(root, subdirectory); childDeps.add( getSkyKeyForSubdirectory( recursivePkgKey.getRepository(), subdirectoryRootedPath, excludedSubdirectoriesBeneathThisSubdirectory)); } Map<SkyKey, SkyValue> subdirectorySkyValues; if (pkgLookupValue.packageExists() && pkgLookupValue.getRoot().equals(root)) { SkyKey packageKey = PackageValue.key(packageId); Map<SkyKey, ValueOrException<NoSuchPackageException>> dependentSkyValues = env.getValuesOrThrow( Iterables.concat(childDeps, ImmutableList.of(packageKey)), NoSuchPackageException.class); if (env.valuesMissing()) { return null; } Package pkg = null; try { PackageValue pkgValue = (PackageValue) dependentSkyValues.get(packageKey).get(); if (pkgValue == null) { return null; } pkg = pkgValue.getPackage(); if (pkg.containsErrors()) { env.getListener() .handle(Event.error("package contains errors: " + rootRelativePath.getPathString())); } } catch (NoSuchPackageException e) { // The package had errors, but don't fail-fast as there might be subpackages below the // current directory. env.getListener() .handle(Event.error("package contains errors: " + rootRelativePath.getPathString())); } if (pkg != null) { visitor.visitPackageValue(pkg, env); if (env.valuesMissing()) { return null; } } ImmutableMap.Builder<SkyKey, SkyValue> subdirectoryBuilder = ImmutableMap.builder(); for (Map.Entry<SkyKey, ValueOrException<NoSuchPackageException>> entry : Maps.filterKeys(dependentSkyValues, Predicates.not(Predicates.equalTo(packageKey))) .entrySet()) { try { subdirectoryBuilder.put(entry.getKey(), entry.getValue().get()); } catch (NoSuchPackageException e) { // ignored. } } subdirectorySkyValues = subdirectoryBuilder.build(); } else { subdirectorySkyValues = env.getValues(childDeps); } if (env.valuesMissing()) { return null; } return aggregateWithSubdirectorySkyValues(visitor, subdirectorySkyValues); }