Add ability to specify target for project. Summary: First bit of slices.
diff --git a/src/com/facebook/buck/cli/ProjectCommand.java b/src/com/facebook/buck/cli/ProjectCommand.java index 55b4b43..008232e 100644 --- a/src/com/facebook/buck/cli/ProjectCommand.java +++ b/src/com/facebook/buck/cli/ProjectCommand.java
@@ -91,7 +91,8 @@ exitCode = createIntellijProject(project, tempFile, executionContext.getProcessExecutor(), - console.getStdOut()); + console.getStdOut(), + console.getStdErr()); if (exitCode != 0) { return exitCode; } @@ -133,9 +134,10 @@ int createIntellijProject(Project project, File jsonTemplate, ProcessExecutor processExecutor, - PrintStream stdOut) + PrintStream stdOut, + PrintStream stdErr) throws IOException { - return project.createIntellijProject(jsonTemplate, processExecutor, stdOut); + return project.createIntellijProject(jsonTemplate, processExecutor, stdOut, stdErr); } /** @@ -152,12 +154,26 @@ @VisibleForTesting PartialGraph createPartialGraph(RawRulePredicate rulePredicate, ProjectCommandOptions options) throws BuildFileParseException, BuildTargetException, IOException { - return PartialGraph.createPartialGraph( - rulePredicate, - getProjectFilesystem(), - options.getDefaultIncludes(), - getParser(), - getBuckEventBus()); + List<String> argumentsAsBuildTargets = options.getArgumentsFormattedAsBuildTargets(); + + if (argumentsAsBuildTargets.isEmpty()) { + return PartialGraph.createPartialGraph( + rulePredicate, + getProjectFilesystem(), + options.getDefaultIncludes(), + getParser(), + getBuckEventBus()); + } else { + // If build targets were specified, generate a partial intellij project that contains the + // files needed to build the build targets specified. + ImmutableList<BuildTarget> targets = getBuildTargets(argumentsAsBuildTargets); + + return PartialGraph.createPartialGraphFromRoots(targets, + rulePredicate, + options.getDefaultIncludes(), + getParser(), + getBuckEventBus()); + } } @Override
diff --git a/src/com/facebook/buck/cli/ProjectCommandOptions.java b/src/com/facebook/buck/cli/ProjectCommandOptions.java index 1ec546c..78ab8a3 100644 --- a/src/com/facebook/buck/cli/ProjectCommandOptions.java +++ b/src/com/facebook/buck/cli/ProjectCommandOptions.java
@@ -23,6 +23,7 @@ import com.google.common.collect.ImmutableMap; import com.google.common.collect.Lists; +import org.kohsuke.args4j.Argument; import org.kohsuke.args4j.Option; import java.util.List; @@ -33,10 +34,25 @@ private String target; + @Argument + private List<String> arguments = Lists.newArrayList(); + ProjectCommandOptions(BuckConfig buckConfig) { super(buckConfig); } + public List<String> getArguments() { + return arguments; + } + + public void setArguments(List<String> arguments) { + this.arguments = arguments; + } + + public List<String> getArgumentsFormattedAsBuildTargets() { + return getCommandLineBuildTargetNormalizer().normalizeAll(getArguments()); + } + public String getTarget() { return target; }
diff --git a/src/com/facebook/buck/command/Project.java b/src/com/facebook/buck/command/Project.java index 6e43b18..1cc14cc 100644 --- a/src/com/facebook/buck/command/Project.java +++ b/src/com/facebook/buck/command/Project.java
@@ -48,6 +48,7 @@ import com.facebook.buck.util.KeystoreProperties; import com.facebook.buck.util.ProcessExecutor; import com.facebook.buck.util.ProjectFilesystem; +import com.facebook.buck.util.Verbosity; import com.fasterxml.jackson.annotation.JsonInclude; import com.fasterxml.jackson.annotation.JsonInclude.Include; import com.fasterxml.jackson.annotation.JsonProperty; @@ -160,14 +161,15 @@ public int createIntellijProject(File jsonTempFile, ProcessExecutor processExecutor, - PrintStream stdOut) throws IOException { + PrintStream stdOut, + PrintStream stdErr) throws IOException { List<Module> modules = createModulesForProjectConfigs(); writeJsonConfig(jsonTempFile, modules); List<String> modifiedFiles = Lists.newArrayList(); // Process the JSON config to generate the .xml and .iml files for IntelliJ. - ExitCodeAndStdOut result = processJsonConfig(jsonTempFile); + ExitCodeAndOutput result = processJsonConfig(jsonTempFile); if (result.exitCode != 0) { return result.exitCode; } else { @@ -202,6 +204,8 @@ SortedSet<String> modifiedFilesInSortedForder = Sets.newTreeSet(modifiedFiles); stdOut.printf("MODIFIED FILES:\n%s\n", Joiner.on('\n').join(modifiedFilesInSortedForder)); } + // Blit stderr from intellij.py to parent stderr. + stdErr.print(result.stdErr); return 0; } @@ -905,7 +909,7 @@ } } - private ExitCodeAndStdOut processJsonConfig(File jsonTempFile) throws IOException { + private ExitCodeAndOutput processJsonConfig(File jsonTempFile) throws IOException { final ImmutableList<String> args = ImmutableList.of( pythonInterpreter, PATH_TO_INTELLIJ_PY, jsonTempFile.getAbsolutePath()); @@ -925,7 +929,7 @@ Console console = executionContext.getConsole(); Console childConsole = new Console( - console.getVerbosity(), + Verbosity.SILENT, console.getStdOut(), console.getStdErr(), Ansi.withoutTty()); @@ -934,15 +938,17 @@ .setConsole(childConsole) .build(); int exitCode = command.execute(childContext); - return new ExitCodeAndStdOut(exitCode, command.getStdout()); + return new ExitCodeAndOutput(exitCode, command.getStdout(), command.getStderr()); } - private static class ExitCodeAndStdOut { + private static class ExitCodeAndOutput { private final int exitCode; private final String stdOut; - ExitCodeAndStdOut(int exitCode, String stdOut) { + private final String stdErr; + ExitCodeAndOutput(int exitCode, String stdOut, String stdErr) { this.exitCode = exitCode; this.stdOut = Preconditions.checkNotNull(stdOut); + this.stdErr = Preconditions.checkNotNull(stdErr); } }
diff --git a/src/com/facebook/buck/command/intellij.py b/src/com/facebook/buck/command/intellij.py index 2c166da..c0d5185 100644 --- a/src/com/facebook/buck/command/intellij.py +++ b/src/com/facebook/buck/command/intellij.py
@@ -1,6 +1,9 @@ import errno +import fnmatch import json import os +import re +import subprocess import sys @@ -105,6 +108,9 @@ # and no files will need to be written. MODIFIED_FILES = [] +# Files that are part of the project being run. We will delete all .iml files +# that are not checked in and not in this set. +PROJECT_FILES = set() def write_modules(modules): """Writes one XML file for each module.""" @@ -348,6 +354,7 @@ def write_file_if_changed(path, content): + PROJECT_FILES.add(path) if os.path.exists(path): file_content_as_string = open(path, 'r').read() needs_update = content.strip() != file_content_as_string.strip() @@ -372,8 +379,31 @@ pass else: raise +def clean_old_files(): + if os.path.isdir('.git'): + try: + files_to_clean = subprocess.check_output(['git', 'ls-files', '--other']) + for file_name in files_to_clean.splitlines(): + if file_name.endswith('.iml') and file_name not in PROJECT_FILES: + os.remove(file_name) + return + except Exception as e: + pass + if __name__ == '__main__': + if not os.path.isdir('.git'): + for root, dirnames, filenames in os.walk('.'): + if fnmatch.filter(filenames, '*.iml'): + sys.stderr.write('\n'.join( + [ ' :: "buck project" run from a directory not under Git source', + ' :: control. If invoking buck project with an argument, we are', + ' :: not able to remove old .iml files, which can result in', + ' :: IntelliJ being in a bad state. Please close and re-open', + ' :: IntelliJ if it\'s open.' ])) + sys.stderr.flush() + break + json_file = sys.argv[1] parsed_json = json.load(open(json_file, 'r')) @@ -384,6 +414,8 @@ write_modules(modules) write_all_modules(modules) write_run_configs() + if PROJECT_FILES: + clean_old_files() # Write the list of modified files to stdout for path in MODIFIED_FILES: print path
diff --git a/src/com/facebook/buck/parser/Parser.java b/src/com/facebook/buck/parser/Parser.java index 97c8f0b..740c7ac 100644 --- a/src/com/facebook/buck/parser/Parser.java +++ b/src/com/facebook/buck/parser/Parser.java
@@ -605,6 +605,32 @@ return filterTargets(filter); } + + /** + * Takes a sequence of build targets and parses all of the build files that contain them and their + * transitive deps, producing a collection of "raw rules" that have been produced from the build + * files. The specified {@link RawRulePredicate} is applied to this collection. This method + * returns the collection of {@link BuildTarget}s that correspond to the raw rules that were + * matched by the predicate. + * <p> + * Because {@code project_config} rules are not transitive dependencies of rules such as + * {@code android_binary}, but are defined in the same build files as the transitive + * dependencies of an {@code android_binary}, this method is helpful in finding all of the + * {@code project_config} rules needed to produce an IDE configuration to build said + * {@code android_binary}. See {@link ProjectCommand#predicate} for an example of such + * a {@link RawRulePredicate}. + */ + public synchronized List<BuildTarget> filterTargetsInProjectFromRoots( + Iterable<BuildTarget> roots, + Iterable<String> defaultIncludes, + BuckEventBus eventBus, + RawRulePredicate filter) + throws BuildFileParseException, BuildTargetException, IOException { + parseBuildFilesForTargets(roots, defaultIncludes, eventBus); + + return filterTargets(filter); + } + /** * Called when file change events are posted to the file change EventBus to invalidate cached * build rules if required.
diff --git a/src/com/facebook/buck/parser/PartialGraph.java b/src/com/facebook/buck/parser/PartialGraph.java index d37275e..f05262b 100644 --- a/src/com/facebook/buck/parser/PartialGraph.java +++ b/src/com/facebook/buck/parser/PartialGraph.java
@@ -69,15 +69,44 @@ Iterable<String> includes, Parser parser, BuckEventBus eventBus) throws BuildTargetException, BuildFileParseException, IOException { - - Preconditions.checkNotNull(parser); + Preconditions.checkNotNull(predicate); List<BuildTarget> targets = parser.filterAllTargetsInProject(filesystem, includes, predicate); + return parseAndCreateGraphFromTargets(targets, includes, parser, eventBus); + } + + /** + * Creates a partial graph of all {@link BuildRule}s that are transitive dependencies of (rules + * that pass {@code predicate} and are contained in BUCK files that contain transitive + * dependencies of the {@link BuildTarget}s defined in {@code roots}). + */ + public static PartialGraph createPartialGraphFromRoots( + Iterable<BuildTarget> roots, + RawRulePredicate predicate, + Iterable<String> includes, + Parser parser, + BuckEventBus eventBus) throws BuildTargetException, BuildFileParseException, IOException { + Preconditions.checkNotNull(predicate); + + List<BuildTarget> targets = parser.filterTargetsInProjectFromRoots( + roots, includes, eventBus, predicate); + + return parseAndCreateGraphFromTargets(targets, includes, parser, eventBus); + } + + private static PartialGraph parseAndCreateGraphFromTargets( + Iterable<BuildTarget> targets, + Iterable<String> includes, + Parser parser, + BuckEventBus eventBus) throws BuildTargetException, BuildFileParseException, IOException { + + Preconditions.checkNotNull(parser); + // Now that the Parser is loaded up with the set of all build rules, use it to create a // DependencyGraph of only the targets we want to build. DependencyGraph graph = parser.parseBuildFilesForTargets(targets, includes, eventBus); - return new PartialGraph(graph, targets); + return new PartialGraph(graph, ImmutableList.copyOf(targets)); } }
diff --git a/test/com/facebook/buck/cli/ProjectCommandTest.java b/test/com/facebook/buck/cli/ProjectCommandTest.java index 4602a7d..a235e25 100644 --- a/test/com/facebook/buck/cli/ProjectCommandTest.java +++ b/test/com/facebook/buck/cli/ProjectCommandTest.java
@@ -188,12 +188,14 @@ int createIntellijProject(Project project, File jsonTemplate, ProcessExecutor processExecutor, - PrintStream stdOut) + PrintStream stdOut, + PrintStream stdErr) throws IOException { assertNotNull(project); assertNotNull(jsonTemplate); assertNotNull(processExecutor); assertNotNull(stdOut); + assertNotNull(stdErr); return 0; }
diff --git a/test/com/facebook/buck/cli/ProjectIntegrationTest.java b/test/com/facebook/buck/cli/ProjectIntegrationTest.java index ec55e3f..8457953 100644 --- a/test/com/facebook/buck/cli/ProjectIntegrationTest.java +++ b/test/com/facebook/buck/cli/ProjectIntegrationTest.java
@@ -16,8 +16,10 @@ package com.facebook.buck.cli; +import static org.hamcrest.Matchers.containsString; +import static org.hamcrest.Matchers.not; import static org.junit.Assert.assertEquals; - +import static org.junit.Assert.assertThat; import com.facebook.buck.testutil.integration.DebuggableTemporaryFolder; import com.facebook.buck.testutil.integration.ProjectWorkspace; import com.facebook.buck.testutil.integration.ProjectWorkspace.ProcessResult; @@ -62,6 +64,57 @@ "modules/tip/module_modules_tip.iml" ) + '\n', result.getStdout()); + + assertThat( + "`buck project` should contain warning about being run from directory without git.", + result.getStderr(), + not(containsString(Joiner.on('\n').join( + " :: \"buck project\" run from a directory not under Git source", + " :: control. If invoking buck project with an argument, we are", + " :: not able to remove old .iml files, which can result in", + " :: IntelliJ being in a bad state. Please close and re-open", + " :: IntelliJ if it's open.")))); + } + + /** + * Verify that if we build a project by specifying a target, the resulting project only contains + * the transitive deps of that target. In this example, that means everything except + * //modules/tip. + */ + @Test + public void testBuckProjectSlice() throws IOException { + ProjectWorkspace workspace = TestDataHelper.createProjectWorkspaceForScenario( + this, "project_slice", temporaryFolder); + workspace.setUp(); + + ProcessResult result = workspace.runBuckCommand("project", "//modules/dep1:dep1"); + result.assertExitCode("buck project should exit cleanly", 0); + + workspace.verify(); + + assertEquals( + "`buck project` should report the files it modified.", + Joiner.on('\n').join( + "MODIFIED FILES:", + ".idea/compiler.xml", + ".idea/libraries/libs_guava_jar.xml", + ".idea/libraries/libs_jsr305_jar.xml", + ".idea/libraries/libs_junit_jar.xml", + ".idea/modules.xml", + ".idea/runConfigurations/Debug_Buck_test.xml", + "modules/dep1/module_modules_dep1.iml" + ) + '\n', + result.getStdout()); + + assertThat( + "`buck project` should contain warning about being run from directory without git.", + result.getStderr(), + containsString(Joiner.on('\n').join( + " :: \"buck project\" run from a directory not under Git source", + " :: control. If invoking buck project with an argument, we are", + " :: not able to remove old .iml files, which can result in", + " :: IntelliJ being in a bad state. Please close and re-open", + " :: IntelliJ if it's open."))); } /**
diff --git a/test/com/facebook/buck/cli/testdata/project_slice/.buckconfig b/test/com/facebook/buck/cli/testdata/project_slice/.buckconfig new file mode 100644 index 0000000..23528f4 --- /dev/null +++ b/test/com/facebook/buck/cli/testdata/project_slice/.buckconfig
@@ -0,0 +1,2 @@ +[color] + ui = false
diff --git a/test/com/facebook/buck/cli/testdata/project_slice/.idea/modules.xml.expected b/test/com/facebook/buck/cli/testdata/project_slice/.idea/modules.xml.expected new file mode 100644 index 0000000..0724339 --- /dev/null +++ b/test/com/facebook/buck/cli/testdata/project_slice/.idea/modules.xml.expected
@@ -0,0 +1,8 @@ +<?xml version="1.0" encoding="UTF-8"?> +<project version="4"> + <component name="ProjectModuleManager"> + <modules> + <module fileurl="file://$PROJECT_DIR$/modules/dep1/module_modules_dep1.iml" filepath="$PROJECT_DIR$/modules/dep1/module_modules_dep1.iml" /> + </modules> + </component> +</project>
diff --git a/test/com/facebook/buck/cli/testdata/project_slice/libs/BUCK b/test/com/facebook/buck/cli/testdata/project_slice/libs/BUCK new file mode 100644 index 0000000..5e53ca8 --- /dev/null +++ b/test/com/facebook/buck/cli/testdata/project_slice/libs/BUCK
@@ -0,0 +1,23 @@ +prebuilt_jar( + name = 'guava', + binary_jar = 'guava.jar', + visibility = [ + 'PUBLIC', + ], +) + +prebuilt_jar( + name = 'jsr305', + binary_jar = 'jsr305.jar', + visibility = [ + 'PUBLIC', + ], +) + +prebuilt_jar( + name = 'junit', + binary_jar = 'junit.jar', + visibility = [ + 'PUBLIC', + ], +)
diff --git a/test/com/facebook/buck/cli/testdata/project_slice/libs/guava.jar b/test/com/facebook/buck/cli/testdata/project_slice/libs/guava.jar new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/test/com/facebook/buck/cli/testdata/project_slice/libs/guava.jar
diff --git a/test/com/facebook/buck/cli/testdata/project_slice/libs/jsr305.jar b/test/com/facebook/buck/cli/testdata/project_slice/libs/jsr305.jar new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/test/com/facebook/buck/cli/testdata/project_slice/libs/jsr305.jar
diff --git a/test/com/facebook/buck/cli/testdata/project_slice/libs/junit.jar b/test/com/facebook/buck/cli/testdata/project_slice/libs/junit.jar new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/test/com/facebook/buck/cli/testdata/project_slice/libs/junit.jar
diff --git a/test/com/facebook/buck/cli/testdata/project_slice/modules/dep1/BUCK b/test/com/facebook/buck/cli/testdata/project_slice/modules/dep1/BUCK new file mode 100644 index 0000000..32dc7e2 --- /dev/null +++ b/test/com/facebook/buck/cli/testdata/project_slice/modules/dep1/BUCK
@@ -0,0 +1,28 @@ +android_library( + name = 'dep1', + srcs = glob(['src/**/*.java']), + deps = [ + '//libs:guava', + '//libs:jsr305', + ], + visibility = [ + 'PUBLIC', + ], +) + +java_test( + name = 'test', + srcs = glob(['test/**/*Test.java']), + deps = [ + ':dep1', + '//libs:guava', + '//libs:junit', + ], +) + +project_config( + src_target = ':dep1', + src_roots = [ 'src' ], + test_target = ':test', + test_roots = [ 'test' ], +)
diff --git a/test/com/facebook/buck/cli/testdata/project_slice/modules/dep1/module_modules_dep1.iml.expected b/test/com/facebook/buck/cli/testdata/project_slice/modules/dep1/module_modules_dep1.iml.expected new file mode 100644 index 0000000..66d9ff0 --- /dev/null +++ b/test/com/facebook/buck/cli/testdata/project_slice/modules/dep1/module_modules_dep1.iml.expected
@@ -0,0 +1,46 @@ +<?xml version="1.0" encoding="UTF-8"?> +<module type="JAVA_MODULE" version="4"> + <component name="FacetManager"> + <facet type="android" name="Android"> + <configuration> + <option name="GEN_FOLDER_RELATIVE_PATH_APT" value="/../../buck-out/android/modules/dep1/gen" /> + <option name="GEN_FOLDER_RELATIVE_PATH_AIDL" value="/../../buck-out/android/modules/dep1/gen" /> + <option name="MANIFEST_FILE_RELATIVE_PATH" value="/AndroidManifest.xml" /> + <option name="RES_FOLDER_RELATIVE_PATH" value="/res" /> + <option name="ASSETS_FOLDER_RELATIVE_PATH" value="/assets" /> + <option name="LIBS_FOLDER_RELATIVE_PATH" value="/libs" /> + <option name="USE_CUSTOM_APK_RESOURCE_FOLDER" value="false" /> + <option name="CUSTOM_APK_RESOURCE_FOLDER" value="" /> + <option name="USE_CUSTOM_COMPILER_MANIFEST" value="false" /> + <option name="CUSTOM_COMPILER_MANIFEST" value="" /> + <option name="APK_PATH" value="" /> + <option name="LIBRARY_PROJECT" value="true" /> + <option name="RUN_PROCESS_RESOURCES_MAVEN_TASK" value="true" /> + <option name="GENERATE_UNSIGNED_APK" value="false" /> + <option name="CUSTOM_DEBUG_KEYSTORE_PATH" value="" /> + <option name="PACK_TEST_CODE" value="false" /> + <option name="RUN_PROGUARD" value="false" /> + <option name="PROGUARD_CFG_PATH" value="/proguard.cfg" /> + <resOverlayFolders /> + <includeSystemProguardFile>false</includeSystemProguardFile> + <includeAssetsFromLibraries>true</includeAssetsFromLibraries> + <additionalNativeLibs /> + </configuration> + </facet> + </component> + <component name="NewModuleRootManager" inherit-compiler-output="true"> + <content url="file://$MODULE_DIR$/../../buck-out/android/modules/dep1/gen"> + <sourceFolder url="file://$MODULE_DIR$/../../buck-out/android/modules/dep1/gen" isTestSource="false" /> + </content> + <content url="file://$MODULE_DIR$"> + <sourceFolder url="file://$MODULE_DIR$/test" isTestSource="true" /> + <sourceFolder url="file://$MODULE_DIR$/src" isTestSource="false" /> + <sourceFolder url="file://$MODULE_DIR$/gen" isTestSource="false" /> + </content> + <orderEntry type="sourceFolder" forTests="false" /> + <orderEntry type="library" exported="" scope="TEST" name="libs_junit_jar" level="project" /> + <orderEntry type="library" exported="" name="libs_guava_jar" level="project" /> + <orderEntry type="library" exported="" name="libs_jsr305_jar" level="project" /> + <orderEntry type="inheritedJdk" /> + </component> +</module>
diff --git a/test/com/facebook/buck/cli/testdata/project_slice/modules/tip/BUCK b/test/com/facebook/buck/cli/testdata/project_slice/modules/tip/BUCK new file mode 100644 index 0000000..3dba30b --- /dev/null +++ b/test/com/facebook/buck/cli/testdata/project_slice/modules/tip/BUCK
@@ -0,0 +1,27 @@ +android_library( + name = 'tip', + srcs = glob(['src/**/*.java']), + deps = [ + '//libs:guava', + '//libs:jsr305', + '//modules/dep1:dep1', + ], +) + +java_test( + name = 'test', + srcs = glob(['test/**/*Test.java']), + deps = [ + ':tip', + '//libs:guava', + '//libs:jsr305', + '//libs:junit', + ], +) + +project_config( + src_target = ':tip', + src_roots = [ 'src' ], + test_target = ':test', + test_roots = [ 'test' ], +)
diff --git a/test/com/facebook/buck/cli/testdata/project_slice/modules/tip/module_modules_tip.iml b/test/com/facebook/buck/cli/testdata/project_slice/modules/tip/module_modules_tip.iml new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/test/com/facebook/buck/cli/testdata/project_slice/modules/tip/module_modules_tip.iml