com.google.devtools.build.lib.buildtool.SymlinkForest.java Source code

Java tutorial

Introduction

Here is the source code for com.google.devtools.build.lib.buildtool.SymlinkForest.java

Source

// Copyright 2016 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.buildtool;

import com.google.common.annotations.VisibleForTesting;
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.ImmutableSet;
import com.google.common.collect.Maps;
import com.google.common.collect.Sets;
import com.google.devtools.build.lib.cmdline.Label;
import com.google.devtools.build.lib.cmdline.PackageIdentifier;
import com.google.devtools.build.lib.concurrent.ThreadSafety;
import com.google.devtools.build.lib.util.Preconditions;
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.Map;
import java.util.Set;
import java.util.logging.Level;
import java.util.logging.Logger;

/**
 * Creates a symlink forest based on a package path map.
 */
class SymlinkForest {

    private static final Logger LOG = Logger.getLogger(SymlinkForest.class.getName());
    private static final boolean LOG_FINER = LOG.isLoggable(Level.FINER);

    private final ImmutableMap<PackageIdentifier, Path> packageRoots;
    private final Path workspace;
    private final String workspaceName;
    private final String productName;
    private final String[] prefixes;

    SymlinkForest(ImmutableMap<PackageIdentifier, Path> packageRoots, Path workspace, String productName,
            String workspaceName) {
        this.packageRoots = packageRoots;
        this.workspace = workspace;
        this.workspaceName = workspaceName;
        this.productName = productName;
        this.prefixes = new String[] { ".", "_", productName + "-" };
    }

    /**
     * Returns the longest prefix from a given set of 'prefixes' that are
     * contained in 'path'. I.e the closest ancestor directory containing path.
     * Returns null if none found.
     * @param path
     * @param prefixes
     */
    @VisibleForTesting
    static PackageIdentifier longestPathPrefix(PackageIdentifier path, ImmutableSet<PackageIdentifier> prefixes) {
        for (int i = path.getPackageFragment().segmentCount(); i >= 0; i--) {
            PackageIdentifier prefix = createInRepo(path, path.getPackageFragment().subFragment(0, i));
            if (prefixes.contains(prefix)) {
                return prefix;
            }
        }
        return null;
    }

    /**
     * Delete all dir trees under a given 'dir' that don't start with one of a set
     * of given 'prefixes'. Does not follow any symbolic links.
     */
    @VisibleForTesting
    @ThreadSafety.ThreadSafe
    static void deleteTreesBelowNotPrefixed(Path dir, String[] prefixes) throws IOException {
        dirloop: for (Path p : dir.getDirectoryEntries()) {
            String name = p.getBaseName();
            for (String prefix : prefixes) {
                if (name.startsWith(prefix)) {
                    continue dirloop;
                }
            }
            FileSystemUtils.deleteTree(p);
        }
    }

    void plantSymlinkForest() throws IOException {
        deleteTreesBelowNotPrefixed(workspace, prefixes);

        // Create a sorted map of all dirs (packages and their ancestors) to sets of their roots.
        // Packages come from exactly one root, but their shared ancestors may come from more.
        // The map is maintained sorted lexicographically, so parents are before their children.
        Map<PackageIdentifier, Set<Path>> dirRootsMap = Maps.newTreeMap();
        for (Map.Entry<PackageIdentifier, Path> entry : packageRoots.entrySet()) {
            PackageIdentifier pkgId = entry.getKey();
            Path pkgRoot = entry.getValue();
            for (int i = 1; i <= pkgId.getPackageFragment().segmentCount(); i++) {
                if (pkgId.equals(Label.EXTERNAL_PACKAGE_IDENTIFIER)) {
                    // This isn't a "real" package, don't add it to the symlink tree.
                    continue;
                }
                PackageIdentifier dir = createInRepo(pkgId, pkgId.getPackageFragment().subFragment(0, i));
                Set<Path> roots = dirRootsMap.get(dir);
                if (roots == null) {
                    roots = Sets.newHashSet();
                    dirRootsMap.put(dir, roots);
                }
                roots.add(pkgRoot);
            }
        }
        // Now add in roots for all non-pkg dirs that are in between two packages, and missed above.
        for (Map.Entry<PackageIdentifier, Set<Path>> entry : dirRootsMap.entrySet()) {
            PackageIdentifier dir = entry.getKey();
            if (!packageRoots.containsKey(dir)) {
                PackageIdentifier pkgId = longestPathPrefix(dir, packageRoots.keySet());
                if (pkgId != null) {
                    entry.getValue().add(packageRoots.get(pkgId));
                }
            }
        }
        // Create output dirs for all dirs that have more than one root and need to be split.
        for (Map.Entry<PackageIdentifier, Set<Path>> entry : dirRootsMap.entrySet()) {
            PackageIdentifier dir = entry.getKey();
            if (!dir.getRepository().isMain()) {
                FileSystemUtils.createDirectoryAndParents(
                        workspace.getRelative(dir.getRepository().getPathUnderExecRoot()));
            }
            if (entry.getValue().size() > 1) {
                if (LOG_FINER) {
                    LOG.finer("mkdir " + workspace.getRelative(dir.getPathUnderExecRoot()));
                }
                FileSystemUtils.createDirectoryAndParents(workspace.getRelative(dir.getPathUnderExecRoot()));
            }
        }

        // Make dir links for single rooted dirs.
        for (Map.Entry<PackageIdentifier, Set<Path>> entry : dirRootsMap.entrySet()) {
            PackageIdentifier dir = entry.getKey();
            Set<Path> roots = entry.getValue();
            // Simple case of one root for this dir.
            if (roots.size() == 1) {
                if (dir.getPackageFragment().segmentCount() > 1 && dirRootsMap.get(getParent(dir)).size() == 1) {
                    continue; // skip--an ancestor will link this one in from above
                }
                // This is the top-most dir that can be linked to a single root. Make it so.
                Path root = roots.iterator().next(); // lone root in set
                if (LOG_FINER) {
                    LOG.finer("ln -s " + root.getRelative(dir.getSourceRoot()) + " "
                            + workspace.getRelative(dir.getPathUnderExecRoot()));
                }
                workspace.getRelative(dir.getPathUnderExecRoot())
                        .createSymbolicLink(root.getRelative(dir.getSourceRoot()));
            }
        }
        // Make links for dirs within packages, skip parent-only dirs.
        for (Map.Entry<PackageIdentifier, Set<Path>> entry : dirRootsMap.entrySet()) {
            PackageIdentifier dir = entry.getKey();
            if (entry.getValue().size() > 1) {
                // If this dir is at or below a package dir, link in its contents.
                PackageIdentifier pkgId = longestPathPrefix(dir, packageRoots.keySet());
                if (pkgId != null) {
                    Path root = packageRoots.get(pkgId);
                    try {
                        Path absdir = root.getRelative(dir.getSourceRoot());
                        if (absdir.isDirectory()) {
                            if (LOG_FINER) {
                                LOG.finer("ln -s " + absdir + "/* " + workspace.getRelative(dir.getSourceRoot())
                                        + "/");
                            }
                            for (Path target : absdir.getDirectoryEntries()) {
                                PathFragment p = target.relativeTo(root);
                                if (!dirRootsMap.containsKey(createInRepo(pkgId, p))) {
                                    //LOG.finest("ln -s " + target + " " + linkRoot.getRelative(p));
                                    workspace.getRelative(p).createSymbolicLink(target);
                                }
                            }
                        } else {
                            LOG.fine("Symlink planting skipping dir '" + absdir + "'");
                        }
                    } catch (IOException e) {
                        e.printStackTrace();
                    }
                    // Otherwise its just an otherwise empty common parent dir.
                }
            }
        }

        for (Map.Entry<PackageIdentifier, Path> entry : packageRoots.entrySet()) {
            PackageIdentifier pkgId = entry.getKey();
            if (!pkgId.getPackageFragment().equals(PathFragment.EMPTY_FRAGMENT)) {
                continue;
            }
            Path execrootDirectory = workspace.getRelative(pkgId.getPathUnderExecRoot());
            // If there were no subpackages, this directory might not exist yet.
            if (!execrootDirectory.exists()) {
                FileSystemUtils.createDirectoryAndParents(execrootDirectory);
            }
            // For the top-level directory, generate symlinks to everything in the directory instead of
            // the directory itself.
            Path sourceDirectory = entry.getValue().getRelative(pkgId.getSourceRoot());
            for (Path target : sourceDirectory.getDirectoryEntries()) {
                String baseName = target.getBaseName();
                Path execPath = execrootDirectory.getRelative(baseName);
                // Create any links that don't exist yet and don't start with bazel-.
                if (!baseName.startsWith(productName + "-") && !execPath.exists()) {
                    execPath.createSymbolicLink(target);
                }
            }
        }

        symlinkCorrectWorkspaceName();
    }

    /**
     * Right now, the execution root is under the basename of the source directory, not the name
     * defined in the WORKSPACE file. Thus, this adds a symlink with the WORKSPACE's workspace name
     * to the old-style execution root.
     * TODO(kchodorow): get rid of this once exec root is always under the WORKSPACE's workspace
     * name.
     * @throws IOException
     */
    private void symlinkCorrectWorkspaceName() throws IOException {
        Path correctDirectory = workspace.getParentDirectory().getRelative(workspaceName);
        if (!correctDirectory.exists()) {
            correctDirectory.createSymbolicLink(workspace);
        }
    }

    private static PackageIdentifier getParent(PackageIdentifier packageIdentifier) {
        Preconditions.checkArgument(packageIdentifier.getPackageFragment().getParentDirectory() != null);
        return createInRepo(packageIdentifier, packageIdentifier.getPackageFragment().getParentDirectory());
    }

    private static PackageIdentifier createInRepo(PackageIdentifier repo, PathFragment packageFragment) {
        return PackageIdentifier.create(repo.getRepository(), packageFragment);
    }
}