/* * Copyright 2017-present Facebook, Inc. * * 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.facebook.buck.ide.intellij.projectview; import static com.facebook.buck.ide.intellij.projectview.Patterns.capture; import static com.facebook.buck.ide.intellij.projectview.Patterns.noncapture; import static com.facebook.buck.ide.intellij.projectview.Patterns.optional; import com.facebook.buck.config.Config; import com.facebook.buck.graph.AbstractBreadthFirstTraversal; import com.facebook.buck.io.ProjectFilesystem; import com.facebook.buck.jvm.java.JavaLibrary; import com.facebook.buck.model.BuildTarget; import com.facebook.buck.model.BuildTargets; import com.facebook.buck.rules.ActionGraphAndResolver; import com.facebook.buck.rules.BuildRule; import com.facebook.buck.rules.BuildRuleResolver; import com.facebook.buck.rules.CommonDescriptionArg; import com.facebook.buck.rules.SourcePath; import com.facebook.buck.rules.SourcePathResolver; import com.facebook.buck.rules.SourcePathRuleFinder; import com.facebook.buck.rules.TargetGraph; import com.facebook.buck.rules.TargetNode; import com.facebook.buck.rules.TargetNodes; import com.facebook.buck.util.DirtyPrintStreamDecorator; import com.google.common.collect.ImmutableSet; import java.io.File; import java.io.FileWriter; import java.io.FilenameFilter; import java.io.IOException; import java.io.Writer; import java.nio.file.Files; import java.nio.file.LinkOption; import java.nio.file.Path; import java.nio.file.Paths; import java.nio.file.attribute.BasicFileAttributes; import java.util.ArrayList; import java.util.Collection; import java.util.Comparator; import java.util.HashMap; import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.Objects; import java.util.Optional; import java.util.Set; import java.util.regex.Matcher; import java.util.regex.Pattern; import java.util.stream.Collectors; import java.util.stream.Stream; import javax.annotation.Nullable; import org.jdom2.Attribute; import org.jdom2.Document; import org.jdom2.Element; import org.jdom2.output.Format; import org.jdom2.output.XMLOutputter; public class ProjectView { // region Public API public static int run( DirtyPrintStreamDecorator stderr, boolean dryRun, boolean withTests, String viewPath, TargetGraph targetGraph, ImmutableSet<BuildTarget> buildTargets, ActionGraphAndResolver actionGraph, Config config) { return new ProjectView( stderr, dryRun, withTests, viewPath, targetGraph, buildTargets, actionGraph, config) .run(); } // endregion Public API // region Private implementation private static final String ANDROID_MANIFEST = "AndroidManifest.xml"; private static final String ANDROID_RES = "android_res"; private static final String ASSETS = "assets"; private static final String CODE_STYLE_SETTINGS = "codeStyleSettings.xml"; private static final String DOT_IDEA = ".idea"; private static final String DOT_XML = ".xml"; private static final String FONTS = "fonts"; private static final String RES = "res"; private final DirtyPrintStreamDecorator stdErr; private final String viewPath; private final boolean dryRun; private final boolean withTests; private final TargetGraph targetGraph; private final ImmutableSet<BuildTarget> buildTargets; private final Config config; private final ActionGraphAndResolver actionGraph; private final Set<BuildTarget> testTargets = new HashSet<>(); /** {@code Sets.union(buildTargets, allTargets)} */ private final Set<BuildTarget> allTargets = new HashSet<>(); private final String repository = new File("").getAbsolutePath(); private ProjectView( DirtyPrintStreamDecorator stdErr, boolean dryRun, boolean withTests, String viewPath, TargetGraph targetGraph, ImmutableSet<BuildTarget> buildTargets, ActionGraphAndResolver actionGraph, Config config) { this.stdErr = stdErr; this.viewPath = viewPath; this.dryRun = dryRun; this.withTests = withTests; this.targetGraph = targetGraph; this.buildTargets = buildTargets; this.actionGraph = actionGraph; this.config = config; } private int run() { if (viewPathIsUnderRepository()) { stderr("\nView directory %s is under the repo directory %s\n", viewPath, repository); return 1; } getTestTargets(); List<String> inputs = getPrunedInputs(); scanExistingView(); List<String> sourceFiles = new ArrayList<>(); for (String input : inputs) { if (input.startsWith("android_res/")) { linkResourceFile(input); } else { sourceFiles.add(input); } } Set<String> roots = generateRoots(sourceFiles); buildRootLinks(roots); writeRootDotIml(sourceFiles, roots, buildDotIdeaFolder(inputs)); buildAllDirectoriesAndSymlinks(); return 0; } private boolean viewPathIsUnderRepository() { Path view = Paths.get(viewPath).toAbsolutePath(); Path repo = Paths.get(repository).toAbsolutePath(); return view.startsWith(repo); } // region getTestTargets private void getTestTargets() { if (withTests) { AbstractBreadthFirstTraversal.<TargetNode<?, ?>>traverse( targetGraph.getAll(buildTargets), node -> { testTargets.addAll(TargetNodes.getTestTargetsForNode(node)); return targetGraph.getAll(node.getBuildDeps()); }); } allTargets.addAll(buildTargets); allTargets.addAll(testTargets); } // endregion getTestTargets // region getPrunedInputs() private List<String> getPrunedInputs() { return pruneInputs(getAllInputs()); } private Collection<String> getAllInputs() { Set<String> inputs = new HashSet<>(); for (TargetNode<?, ?> node : targetGraph.getNodes()) { node.getInputs().forEach(input -> inputs.add(input.toString())); } return inputs .stream() //ignore non-english strings .filter(input -> !(input.contains("/res/values-") && input.endsWith("strings.xml"))) .collect(Collectors.toList()); } private List<String> pruneInputs(Collection<String> allInputs) { Pattern resource = Pattern.compile("/res/(?!(?:values(?:-[^/]+)?)/)"); List<String> result = new ArrayList<>(); Map<String, List<String>> resources = new HashMap<>(); for (String input : allInputs) { Matcher matcher = resource.matcher(input); if (matcher.find()) { String basename = basename(input); List<String> candidates = resources.get(basename); if (candidates == null) { resources.put(basename, candidates = new ArrayList<>()); } candidates.add(input); } else { result.add(input); } } for (Map.Entry<String, List<String>> mapping : resources.entrySet()) { List<String> candidateList = mapping.getValue(); Stream<String> candidateStream = candidateList.stream(); if (candidateList.size() > 1) { candidateStream = candidateStream.sorted(); } result.add(candidateStream.findFirst().get()); } return result; } // endregion getPrunedInputs() // region linkResourceFile private static final String DASH_PART = "-[^/]+"; private static final String NONCAPTURE_DASH_PART = optional(noncapture(DASH_PART)); private static final Patterns SIMPLE_RESOURCE_PATTERNS = Patterns.builder() // These are ordered based on the frequency in two large Android projects. // This ordering will not be ideal for every project, but it's probably not too far off. .add("/res/", capture("drawable", NONCAPTURE_DASH_PART), "/") .add("/res/", capture("layout", NONCAPTURE_DASH_PART), "/") .add("/res/", capture("raw", NONCAPTURE_DASH_PART), "/") .add("/res/", capture("anim", NONCAPTURE_DASH_PART), "/") .add("/res/", capture("xml", NONCAPTURE_DASH_PART), "/") .add("/res/", capture("menu", NONCAPTURE_DASH_PART), "/") .add("/res/", capture("animator"), "/") .build(); private static final String CAPTURE_ALL = capture(".*"); private static final String CAPTURE_DASH_PART = optional(capture(DASH_PART)); private static final Patterns MANGLED_RESOURCE_PATTERNS = Patterns.builder() // These are also ordered based on the frequency in the same two large Android projects. .add("^android_res/", CAPTURE_ALL, "res/(values)", CAPTURE_DASH_PART, "/") .add("^android_res/", CAPTURE_ALL, "res/(color)", CAPTURE_DASH_PART, "/") .build(); // Group 1 has any path under ...//assets/ while group 2 has the filename private static final Patterns ASSETS_RES = Patterns.build("/assets/", capture(noncapture("[^/]+/"), "*"), CAPTURE_ALL); private static final Patterns FONTS_RES = Patterns.build("/fonts/", capture(".*\\.\\w+")); private void linkResourceFile(String input) { // TODO(shemitz) Convert (say) "res/drawable-hdpi/" to "res/drawable/" if (SIMPLE_RESOURCE_PATTERNS.onAnyMatch(input, this::simpleResourceLink)) { return; } if (MANGLED_RESOURCE_PATTERNS.onAnyMatch(input, this::mangledResourceLink)) { return; } if (ASSETS_RES.onAnyMatch(input, this::assetsLink)) { return; } if (FONTS_RES.onAnyMatch(input, this::fontsLink)) { return; } if (input.contains(".")) { stderr("Can't handle %s\n", input); } } private void simpleResourceLink(Matcher match, String input) { String name = basename(input); String directory = fileJoin(viewPath, RES, flattenResourceDirectoryName(match.group(1))); mkdir(directory); symlink(fileJoin(repository, input), fileJoin(directory, name)); } private void mangledResourceLink(Matcher match, String input) { String fileName = basename(input); //its safe to assume input is .xml file String name = fileName.substring(0, fileName.length() - DOT_XML.length()); String path = match.group(1).replace('/', '_'); String configQualifier = match.groupCount() > 2 ? match.group(3) : ""; String directory = fileJoin(viewPath, RES, match.group(2)); mkdir(directory); symlink( fileJoin(repository, input), fileJoin(directory, path + name + configQualifier + DOT_XML)); } private static String flattenResourceDirectoryName(String name) { int dash = name.indexOf('-'); return dash < 0 ? name : name.substring(0, dash); } private void assetsLink(Matcher match, String input) { String inside = match.group(1); // everything between .../assets/ and filename String name = match.group(2); // basename(input) String directory = fileJoin(viewPath, ASSETS, inside); mkdir(directory); symlink(fileJoin(repository, input), fileJoin(directory, name)); } private void fontsLink(Matcher match, String input) { String target = fileJoin(viewPath, FONTS, match.group(1)); String path = dirname(target); mkdir(path); symlink(fileJoin(repository, input), target); } // endregion linkResourceFile // region roots private Set<String> generateRoots(List<String> sourceFiles) { final Set<String> roots = new HashSet<>(); final RootsHelper helper = new RootsHelper(); for (String sourceFile : sourceFiles) { String path = dirname(sourceFile); if (!isNullOrEmpty(path)) { helper.addSourcePath(path); } } final List<String> paths = helper.getSortedSourcePaths(); for (int index = 0, size = paths.size(); index < size; /*increment in loop*/ ) { final String path = paths.get(index); // This folder could be a root, but so could any of its parents. The best root is the one that // requires the fewest excludedFolder tags int lowestCost = helper.excludesUnder(path); String bestRoot = path; String parent = dirname(path); while (!isNullOrEmpty(parent)) { int cost = helper.excludesUnder(parent); if (cost < lowestCost) { lowestCost = cost; bestRoot = parent; } parent = dirname(parent); } roots.add(bestRoot); index += 1; String prefix = bestRoot.endsWith("/") ? bestRoot : bestRoot + '/'; while (index < size && paths.get(index).startsWith(prefix)) { index += 1; } } return roots; } private void buildRootLinks(Set<String> roots) { for (String root : roots) { symlink(fileJoin(repository, root), fileJoin(viewPath, root)); } } /** Maintains a set of source pathes, and a map of paths -> excludes */ private class RootsHelper { private final Set<String> sourcePaths = new HashSet<>(); private final Map<String, Integer> excludes = new HashMap<>(); void addSourcePath(String sourcePath) { sourcePaths.add(sourcePath); } boolean isSourcePath(String sourcePath) { return sourcePaths.contains(sourcePath); } List<String> getSortedSourcePaths() { return sourcePaths.stream().sorted().collect(Collectors.toList()); } int excludesUnder(String path) { if (excludes.containsKey(path)) { return excludes.get(path); } int sum = 0; File absolute = new File(repository, path); String[] files = absolute.list(neitherDotOrDotDot); if (files != null) { for (String entry : files) { String child = fileJoin(path, entry); if (isDirectory(fileJoin(repository, child))) { if (!isSourcePath(child)) { sum += 1; } sum += excludesUnder(child); } } } excludes.put(path, sum); return sum; } } // endregion roots // region .idea folder private static final String BUCK_OUT = "buck-out"; private static final String COMPONENT = "component"; private static final String CONTENT = "content"; private static final String EXCLUDE_FOLDER = "excludeFolder"; private static final String IS_TEST_SOURCE = "isTestSource"; private static final String LIBRARY = "library"; private static final String MODULES = "modules"; private static final String NAME = "name"; private static final String OPTION = "option"; private static final String ORDER_ENTRY = "orderEntry"; private static final String ROOT_IML = "root.iml"; private static final String SOURCE_FOLDER = "sourceFolder"; private static final String TYPE = "type"; private static final String URL = "url"; private static final String VALUE = "value"; private static final String VERSION = "version"; private static final String MODULE_DIR = "$MODULE_DIR$"; private static final String FILE_MODULE_DIR = "file://" + MODULE_DIR; // region XML utilities private enum XML { DECLARATION, NO_DECLARATION } private static Document newDocument(Element root) { return new Document(root); } private void saveDocument(String path, String filename, XML mode, Document document) { if (path != null) { filename = fileJoin(path, filename); } if (dryRun) { stderr("Writing %s\n", filename); return; } Format prettyFormat = Format.getPrettyFormat(); prettyFormat.setOmitDeclaration(mode == XML.NO_DECLARATION); XMLOutputter outputter = new XMLOutputter(prettyFormat); try (Writer writer = new FileWriter(filename)) { outputter.output(document, writer); } catch (IOException e) { e.printStackTrace(); } } private void saveDocument(String path, String filename, XML mode, Element root) { saveDocument(path, filename, mode, newDocument(root)); } private static Element addElement(Element parent, String name, Attribute... attributes) { Element child = newElement(name, attributes); parent.addContent(child); return child; } private static Element addElement(Element parent, String name, List<Attribute> attributes) { return addElement(parent, name, attributes.toArray(new Attribute[attributes.size()])); } private static Attribute attribute(String name, String value) { return new Attribute(name, value); } private static Attribute attribute(String name, Object value) { return attribute(name, value.toString()); } private static Element newElement(String name, Attribute attribute) { Element element = new Element(name); element.setAttribute(attribute); return element; } private static Element newElement(String name, Attribute... attributes) { Element element = new Element(name); if (attributes != null) { for (Attribute attribute : attributes) { element.setAttribute(attribute); } } return element; } // endregion XML utilities private List<String> buildDotIdeaFolder(List<String> inputs) { String dotIdea = fileJoin(viewPath, DOT_IDEA); immediateMkdir(dotIdea); writeModulesXml(dotIdea); writeMiscXml(dotIdea); symlink( fileJoin(repository, DOT_IDEA, CODE_STYLE_SETTINGS), fileJoin(dotIdea, CODE_STYLE_SETTINGS)); return buildDotIdeaDotLibrariesFolder(dotIdea, inputs); } private void writeModulesXml(String dotIdea) { String filepath = fileJoin("$PROJECT_DIR$", ROOT_IML); String fileurl = "file://" + filepath; Element project = newElement("project", attribute(VERSION, 4)); Element component = addElement(project, COMPONENT, attribute(NAME, "ProjectModuleManager")); Element modules = addElement(component, MODULES); addElement( modules, "module", attribute("fileurl", fileurl), attribute("filepath", filepath), attribute("group", MODULES)); saveDocument(dotIdea, "modules.xml", XML.DECLARATION, project); } private void writeMiscXml(String dotIdea) { Element project = newElement("project", attribute(VERSION, 4)); addElement( project, COMPONENT, attribute(NAME, "FrameworkDetectionExcludesConfiguration"), attribute("detection-enabled", false)); String languageLevel = getIntellijSectionValue(INTELLIJ_LANGUAGE_LEVEL, "JDK_1_7"); String jdkName = getIntellijSectionValue(INTELLIJ_JDK_NAME, "Android API 23 Platform"); String jdkType = getIntellijSectionValue(INTELLIJ_JDK_TYPE, "Android SDK"); addElement( project, COMPONENT, attribute(NAME, "ProjectRootManager"), attribute("VERSION", 2), attribute("languageLevel", languageLevel), attribute("assert-keyword", true), attribute("jdk-15", jdk15(languageLevel)), attribute("project-jdk-name", jdkName), attribute("project-jdk-type", jdkType)); saveDocument(dotIdea, "misc.xml", XML.DECLARATION, project); } // region .buckconfig wrappers /** All the values we currently read come from the [intellij] section of the .buckconfig file */ private static final String INTELLIJ_SECTION = "intellij"; // Values in the [intellij] section private static final String INTELLIJ_LANGUAGE_LEVEL = "language_level"; private static final String INTELLIJ_JDK_NAME = "jdk_name"; private static final String INTELLIJ_JDK_TYPE = "jdk_type"; private Optional<String> getIntellijSectionValue(String propertyName) { return config.getValue(INTELLIJ_SECTION, propertyName); } private String getIntellijSectionValue(String propertyName, String defaultValue) { return getIntellijSectionValue(propertyName).orElse(defaultValue); } /** * Tries to parse the language level. Will always return {@code false} if the string doesn't look * like {@code /JDK_\d_\d/}. Otherwise, will return {@code true} iff language level {@literal >=} * 1.5 */ private static boolean jdk15(String languageLevel) { if (languageLevel.length() < 7 || !languageLevel.startsWith("JDK_")) { return false; } if (languageLevel.charAt(3) != '_' || languageLevel.charAt(5) != '_') { return false; } char major = languageLevel.charAt(4); char minor = languageLevel.charAt(6); if (!Character.isDigit(major) || !Character.isDigit(minor)) { return false; } // If we get here, languageLevel looks like /JDK_\d_\d/ int majorValue = Character.getNumericValue(major); int minorValue = Character.getNumericValue(minor); if (majorValue < 1 || minorValue < 1) { // We passed the isDigit() tests, but either MIGHT be 0 ... return false; } if (majorValue > 1) { return true; } // majorValue == 1 return minorValue >= 5; } // endregion .buckconfig wrappers private List<String> buildDotIdeaDotLibrariesFolder(String dotIdea, List<String> inputs) { String libraries = fileJoin(dotIdea, "libraries"); immediateMkdir(libraries); Map<String, List<String>> directories = new HashMap<>(); inputs .stream() .filter((input) -> input.endsWith(".jar")) .forEach( jar -> { String dirname = dirname(jar); String basename = basename(jar); List<String> basenames = directories.get(dirname); if (basenames == null) { basenames = new ArrayList<>(); directories.put(dirname, basenames); } basenames.add(basename); }); List<String> libraryXmls = new ArrayList<>(); for (Map.Entry<String, List<String>> entry : directories.entrySet()) { libraryXmls.add(buildLibraryFile(libraries, entry.getKey(), entry.getValue())); } return libraryXmls; } private String buildLibraryFile(String libraries, String directory, List<String> jars) { String filename = "library_" + directory.replace('-', '_').replace('/', '_'); List<String> urls = jars.stream() .map((jar) -> fileJoin("jar://$PROJECT_DIR$", directory, jar) + "!/") .collect(Collectors.toList()); Element component = newElement(COMPONENT, attribute(NAME, "libraryTable")); Element library = addElement(component, LIBRARY, attribute(NAME, filename)); Element classes = addElement(library, "CLASSES"); // believe it or not, case matters, here! for (String url : urls) { addElement(classes, "root", attribute(URL, url)); } addElement(library, "JAVADOC"); addElement(library, "SOURCES"); saveDocument(libraries, filename + ".xml", XML.NO_DECLARATION, component); return filename; } private void writeRootDotIml( List<String> sourceFiles, Set<String> roots, List<String> libraries) { String buckOut = fileJoin(viewPath, BUCK_OUT); symlink(fileJoin(repository, BUCK_OUT), buckOut); String apkPath = null; Map<BuildTarget, String> outputs = getOutputs(); // Find the 1st target that has output for (BuildTarget target : buildTargets) { String output = outputs.get(target); if (output != null && output.endsWith(".apk")) { apkPath = File.separator + output; break; } } String manifestPath = fileJoin(File.separator, RES, ANDROID_MANIFEST); symlink(fileJoin(repository, ANDROID_RES, ANDROID_MANIFEST), fileJoin(viewPath, manifestPath)); Element module = newElement("module", attribute(TYPE, "JAVA_MODULE"), attribute(VERSION, 4)); Element facetManager = addElement(module, COMPONENT, attribute(NAME, "FacetManager")); Element facet = addElement(facetManager, "facet", attribute(TYPE, "android"), attribute(NAME, "Android")); Element configuration = addElement(facet, "configuration"); String genFolder = fileJoin(File.separator, BUCK_OUT, "gen"); addElement( configuration, OPTION, attribute(NAME, "GEN_FOLDER_RELATIVE_PATH_APT"), attribute(VALUE, genFolder)); addElement( configuration, OPTION, attribute(NAME, "GEN_FOLDER_RELATIVE_PATH_AIDL"), attribute(VALUE, fileJoin(genFolder, "aidl"))); addElement( configuration, OPTION, attribute(NAME, "MANIFEST_FILE_RELATIVE_PATH"), attribute(VALUE, manifestPath)); addElement( configuration, OPTION, attribute(NAME, "RES_FOLDERS_RELATIVE_PATH"), attribute(VALUE, "/res")); if (apkPath != null) { addElement(configuration, OPTION, attribute(NAME, "APK_PATH"), attribute(VALUE, apkPath)); } addElement( configuration, OPTION, attribute(NAME, "ENABLE_SOURCES_AUTOGENERATION"), attribute(VALUE, true)); addElement(configuration, "includeAssetsFromLibraries").addContent("true"); Element rootManager = addElement( module, COMPONENT, attribute(NAME, "NewModuleRootManager"), attribute("inherit-compiler-output", true)); addElement(rootManager, "exclude-output"); Element folders = addElement(rootManager, CONTENT, attribute(URL, FILE_MODULE_DIR)); Set<String> sourceFolders = sourceFiles.stream().map((folder) -> dirname(folder)).collect(Collectors.toSet()); sourceFolders.remove(null); for (String source : sortSourceFolders(sourceFolders)) { List<Attribute> attributes = new ArrayList<>(3); attributes.add(attribute(URL, fileJoin(FILE_MODULE_DIR, source))); attributes.add(attribute(IS_TEST_SOURCE, false)); String packagePrefix = getPackage(fileJoin(repository, source)); if (packagePrefix != null) { attributes.add(attribute("packagePrefix", packagePrefix)); } addElement(folders, SOURCE_FOLDER, attributes); } for (String excluded : getExcludedFolders(sourceFolders, roots)) { addElement(folders, EXCLUDE_FOLDER, attribute(URL, fileJoin(FILE_MODULE_DIR, excluded))); } addElement(rootManager, ORDER_ENTRY, attribute(TYPE, "inheritedJdk")); addElement( rootManager, ORDER_ENTRY, attribute(TYPE, SOURCE_FOLDER), attribute("forTests", false)); for (String library : libraries) { addElement( rootManager, ORDER_ENTRY, attribute(TYPE, LIBRARY), attribute(NAME, library), attribute("level", "project")); } for (String relativeFolder : getAnnotationAndGeneratedFolders()) { String folder = fileJoin(FILE_MODULE_DIR, relativeFolder); Attribute url = attribute(URL, folder); Element content = addElement(rootManager, CONTENT, url); addElement( content, SOURCE_FOLDER, url.clone(), attribute(IS_TEST_SOURCE, false), attribute("generated", true)); } saveDocument(viewPath, ROOT_IML, XML.DECLARATION, module); } private Map<BuildTarget, String> getOutputs() { Map<BuildTarget, String> outputs = new HashMap<>(buildTargets.size()); BuildRuleResolver ruleResolver = actionGraph.getResolver(); SourcePathResolver pathResolver = new SourcePathResolver(new SourcePathRuleFinder(ruleResolver)); for (BuildTarget target : buildTargets) { BuildRule rule = ruleResolver.getRule(target); SourcePath sourcePathToOutput = rule.getSourcePathToOutput(); if (sourcePathToOutput == null) { continue; } Path outputPath = pathResolver.getRelativePath(sourcePathToOutput); outputs.put(target, outputPath.toString()); } return outputs; } private List<String> sortSourceFolders(Set<String> sourceFolders) { return sourceFolders.stream().sorted().collect(Collectors.toList()); } private static Set<String> getExcludedFolders(Set<String> sourceFolders, Set<String> roots) { Set<String> rootFolders = allFoldersUnder(roots); rootFolders.removeAll(sourceFolders); return rootFolders; } private static Set<String> allFoldersUnder(Set<String> roots) { Set<String> result = new HashSet<>(); for (String root : roots) { result.addAll(foldersUnder(root)); } return result; } private static Set<String> foldersUnder(String root) { // TODO(shemitz) This really should use Files.find() ... Set<String> result = new HashSet<>(); File directory = new File(root); if (directory.isDirectory()) { String[] children = directory.list(neitherDotOrDotDot); if (children != null) { for (String child : children) { String qualified = fileJoin(root, child); if (isDirectory(qualified)) { result.add(qualified); result.addAll(foldersUnder(qualified)); } } } } return result; } private static final Pattern PACKAGE = Pattern.compile("package\\s+([\\w\\.]+);"); @Nullable private static String getPackage(String path) { File folder = new File(path); File[] files = folder.listFiles((child) -> child.isFile() && child.getName().endsWith(".java")); if (files != null) { for (File file : files) { String text; try { text = new String(Files.readAllBytes(file.toPath())); } catch (IOException e) { continue; } Matcher matcher = PACKAGE.matcher(text); if (matcher.find()) { return matcher.group(1); } } } return null; } private Collection<String> getAnnotationAndGeneratedFolders() { Collection<String> folders = new HashSet<>(); getAnnotationFolders(folders); getGeneratedFolders(folders); return folders.stream().sorted().collect(Collectors.toList()); } private void getAnnotationFolders(Collection<String> folders) { for (BuildRule buildRule : actionGraph.getActionGraph().getNodes()) { if (buildRule instanceof JavaLibrary) { Optional<Path> generatedSourcePath = ((JavaLibrary) buildRule).getGeneratedSourcePath(); if (generatedSourcePath.isPresent()) { folders.add(generatedSourcePath.get().toString()); } } } } private void getGeneratedFolders(Collection<String> folders) { Map<String, String> labelToGeneratedSourcesMap = config.getMap(INTELLIJ_SECTION, "generated_sources_label_map"); Pattern name = Pattern.compile("%name%"); AbstractBreadthFirstTraversal.<TargetNode<?, ?>>traverse( targetGraph.getAll(allTargets), node -> { ProjectFilesystem filesystem = node.getFilesystem(); Set<BuildTarget> buildDeps = node.getBuildDeps(); for (BuildTarget buildTarget : buildDeps) { Object constructorArg = node.getConstructorArg(); if (constructorArg instanceof CommonDescriptionArg) { CommonDescriptionArg commonDescriptionArg = (CommonDescriptionArg) constructorArg; folders.addAll( commonDescriptionArg .getLabels() .stream() .map(labelToGeneratedSourcesMap::get) .filter(Objects::nonNull) .map( pattern -> name.matcher(pattern) .replaceAll(buildTarget.getShortNameAndFlavorPostfix())) .map( (String path) -> BuildTargets.getGenPath(filesystem, buildTarget, path).toString()) .collect(Collectors.toSet())); } } return targetGraph.getAll(buildDeps); }); } // endregion .idea folder // region symlinks, mkdir, and other file utilities /** * This is <em>not</em> all directories in the view; this is all 'terminals' that have symlinks. * That is, if we have {@code foo/bar/baz/link}, we will record {@code foo/bar/baz} but not {@code * foo/bar} or {@code foo}. */ private final Set<Path> existingDirectories = new HashSet<>(); /** basefile -> link */ private final Map<Path, Path> existingSymlinks = new HashMap<>(); private final Set<Path> directoriesToMake = new HashSet<>(); /** basefile -> link */ private final Map<Path, Path> symlinksToCreate = new HashMap<>(); private void scanExistingView() { Path root = Paths.get(viewPath); if (!Files.exists(root)) { return; } try { Files.find( root, Integer.MAX_VALUE, (Path ignored, BasicFileAttributes attributes) -> attributes.isDirectory() || attributes.isSymbolicLink()) .forEach( path -> { if (Files.isDirectory(path, LinkOption.NOFOLLOW_LINKS)) { if (hasSymbolicLink(path)) { existingDirectories.add(path.toAbsolutePath()); } } else if (Files.isSymbolicLink(path)) { try { existingSymlinks.put(Files.readSymbolicLink(path), path); } catch (IOException e) { stderr("'%s' reading %s", e.getMessage(), path); } } }); } catch (IOException e) { stderr("'%s' scanning the existing links and directories\n", e.getMessage()); } } private boolean hasSymbolicLink(Path path) { try { return Files.list(path).anyMatch(p -> Files.isSymbolicLink(p)); } catch (IOException e) { stderr("'%s' enumerating %s", e.getMessage(), path); return true; } } private void buildAllDirectoriesAndSymlinks() { Set<Path> deletedDirectories = new HashSet<>(); // Delete any directories that should no longer exist for (Path path : existingDirectories) { if (!directoriesToMake.contains(path)) { if (dryRun) { stderr("rm -rf %s\n", path); } else { deleteAll(path); deletedDirectories.add(path); } } } // Make any directories that don't already exist for (Path path : directoriesToMake) { if (!existingDirectories.contains(path)) { immediateMkdir(path); } } // Delete any symlinks that should no longer exist; remove existing links from Map existingSymlinks.forEach( (filePath, linkPath) -> { if (linkPath.equals(symlinksToCreate.get(filePath))) { symlinksToCreate.remove(filePath); } else { if (dryRun) { stderr("rm %s\n", linkPath); } else { try { Files.delete(linkPath); } catch (IOException e) { if (!linkInDeletedDirectories(deletedDirectories, linkPath)) { stderr("'%s' deleting symlink %s\n", e.getMessage(), linkPath); } } } } }); // Create any symlinks that don't already exist symlinksToCreate.forEach( (filePath, linkPath) -> { if (dryRun) { stderr("symlink(%s, %s)\n", filePath, linkPath); } else { createSymbolicLink(filePath, linkPath); } }); } private boolean linkInDeletedDirectories(Set<Path> deletedDirectories, Path linkPath) { Path linkDirectory = linkPath; while ((linkDirectory = linkDirectory.getParent()) != null) { if (deletedDirectories.contains(linkDirectory)) { return true; } } return false; } private static String basename(File file) { return file.getName(); } private static String basename(String filename) { return basename(new File(filename)); } private static String dirname(File file) { return file.getParent(); } private static String dirname(String filename) { return dirname(new File(filename)); } private static String fileJoin(String... components) { StringBuilder join = new StringBuilder(); if (components != null) { for (String component : components) { if (needSeparator(join, component)) { join.append(File.separatorChar); } join.append(component); } } return join.toString(); } private static boolean needSeparator(StringBuilder join, String next) { int length = join.length(); if (length == 0) { return false; } if (join.charAt(length - 1) == File.separatorChar) { return false; } return !next.startsWith(File.separator); } private void mkdir(String name) { directoriesToMake.add(Paths.get(name)); } private void immediateMkdir(String path) { immediateMkdir(Paths.get(path)); } private void immediateMkdir(Path path) { if (dryRun) { stderr("mkdir(%s)\n", path); } else { try { Files.createDirectories(path); } catch (IOException e) { stderr("'%s' creating directory %s\n", e.getMessage(), path); } } } private void symlink(String filename, String linkname) { File link = new File(linkname); Path linkPath = link.toPath(); mkdir(dirname(link)); Path filePath = Paths.get(filename); symlinksToCreate.put(filePath, linkPath); } /** Parameter order is compatible with Ruby library code, for porting transparency */ private void createSymbolicLink(Path oldPath, Path newPath) { try { Files.createSymbolicLink(newPath, oldPath); } catch (IOException e) { stderr( "createSymbolicLink(%s, %s)\n%s:\n%s\n\n", oldPath, newPath, e.getClass().getSimpleName(), e.getMessage()); } } private void deleteAll(Path root) { try { Files.walk(root) .sorted(Comparator.reverseOrder()) // foo/bar before foo .forEach( p -> { try { Files.delete(p); } catch (IOException e) { stderr("'%s' deleting %s\n", e.getMessage(), p); } }); } catch (IOException e) { e.printStackTrace(); } } private static boolean isDirectory(String name) { File file = new File(name); return file.isDirectory(); } private static final FilenameFilter neitherDotOrDotDot = new FilenameFilter() { @Override public boolean accept(File dir, String name) { return !(name.equals(".") || name.equals("..")); } }; // endregion symlinks, mkdir, and other file utilities // region Console IO private void stderr(String pattern, Object... parameters) { stdErr.format(pattern, parameters); } // endregion Console IO // region Random crap private static boolean isNullOrEmpty(String s) { return s == null || s.isEmpty(); } // endregion Random crap // endregion Private implementation }