/** * Paths.computeRelativePath(basePathWithSlash, "") generates the relative path from base path of * current build target to the root of the project. * * <p>Paths.computeRelativePath("", basePathWithSlash) generates the relative path from the root * of the project to base path of current build target. * * <p>For example, for the build target in $PROJECT_DIR$/android_res/com/facebook/gifts/, Intellij * will generate $PROJECT_DIR$/buck-out/android/android_res/com/facebook/gifts/gen * * @return the relative path of gen from the base path of current module. */ static String generateRelativeGenPath(String basePathOfModuleWithSlash) { return "/" + Paths.computeRelativePath(basePathOfModuleWithSlash, "") + ANDROID_GEN_DIR + "/" + Paths.computeRelativePath("", basePathOfModuleWithSlash) + "gen"; }
private Module createModuleForProjectConfig(ProjectConfigRule projectConfig) throws IOException { BuildRule projectRule = projectConfig.getProjectRule(); Preconditions.checkState( projectRule instanceof JavaLibraryRule || projectRule instanceof AndroidLibraryRule || projectRule instanceof AndroidResourceRule || projectRule instanceof AndroidBinaryRule || projectRule instanceof NdkLibraryRule, "project_config() does not know how to process a src_target of type %s.", projectRule.getType().getName()); LinkedHashSet<DependentModule> dependencies = Sets.newLinkedHashSet(); final BuildTarget target = projectConfig.getBuildTarget(); Module module = new Module(projectRule, target); module.name = getIntellijNameForRule(projectRule); module.isIntelliJPlugin = projectConfig.getIsIntelliJPlugin(); String relativePath = projectConfig.getBuildTarget().getBasePathWithSlash(); module.pathToImlFile = String.format("%s%s.iml", relativePath, module.name); // List the module source as the first dependency. boolean includeSourceFolder = true; // Do the tests before the sources so they appear earlier in the classpath. When tests are run, // their classpath entries may be deliberately shadowing production classpath entries. // tests folder boolean hasSourceFoldersForTestRule = addSourceFolders( module, projectConfig.getTestRule(), projectConfig.getTestsSourceRoots(), true /* isTestSource */); // test dependencies BuildRule testRule = projectConfig.getTestRule(); if (testRule != null) { walkRuleAndAdd(testRule, true /* isForTests */, dependencies, projectConfig.getSrcRule()); } // src folder boolean hasSourceFoldersForSrcRule = addSourceFolders( module, projectConfig.getSrcRule(), projectConfig.getSourceRoots(), false /* isTestSource */); // At least one of src or tests should contribute a source folder unless this is an // non-library Android project with no source roots specified. if (!hasSourceFoldersForTestRule && !hasSourceFoldersForSrcRule) { includeSourceFolder = false; } // IntelliJ expects all Android projects to have a gen/ folder, even if there is no src/ // directory specified. boolean isAndroidRule = projectRule.isAndroidRule(); if (isAndroidRule) { boolean hasSourceFolders = !module.sourceFolders.isEmpty(); module.sourceFolders.add(SourceFolder.GEN); if (!hasSourceFolders) { includeSourceFolder = true; } } // src dependencies // Note that isForTests is false even if projectRule is the project_config's test_target. walkRuleAndAdd(projectRule, false /* isForTests */, dependencies, projectConfig.getSrcRule()); String basePathWithSlash = projectConfig.getBuildTarget().getBasePathWithSlash(); // Specify another path for intellij to generate gen/ for each android module, // so that it will not disturb our glob() rules. // To specify the location of gen, Intellij requires the relative path from // the base path of current build target. module.moduleGenPath = generateRelativeGenPath(basePathWithSlash); DependentModule jdkDependency; if (isAndroidRule) { // android details if (projectRule instanceof NdkLibraryRule) { NdkLibraryRule ndkLibraryRule = (NdkLibraryRule) projectRule; module.isAndroidLibraryProject = true; module.keystorePath = null; module.nativeLibs = Paths.computeRelativePath(relativePath, ndkLibraryRule.getLibraryPath()); } else if (projectRule instanceof AndroidResourceRule) { AndroidResourceRule androidResourceRule = (AndroidResourceRule) projectRule; module.resFolder = createRelativePath(androidResourceRule.getRes(), target); module.isAndroidLibraryProject = true; module.keystorePath = null; } else if (projectRule instanceof AndroidBinaryRule) { AndroidBinaryRule androidBinaryRule = (AndroidBinaryRule) projectRule; module.resFolder = null; module.isAndroidLibraryProject = false; KeystoreProperties keystoreProperties = KeystoreProperties.createFromPropertiesFile( androidBinaryRule.getPathToKeystoreProperties(), projectFilesystem); // getKeystore() returns a path relative to the project root, but an IntelliJ module // expects the path to the keystore to be relative to the module root, so we strip off the // module path. String keystorePathRelativeToProjectRoot = keystoreProperties.getKeystore(); String keystorePath = keystorePathRelativeToProjectRoot.substring(relativePath.length()); module.keystorePath = keystorePath; } else { module.isAndroidLibraryProject = true; module.keystorePath = null; } module.hasAndroidFacet = true; module.proguardConfigPath = null; // If there is a default AndroidManifest.xml specified in .buckconfig, use it if // AndroidManifest.xml is not present in the root of the [Android] IntelliJ module. if (pathToDefaultAndroidManifest.isPresent()) { String androidManifest = basePathWithSlash + "AndroidManifest.xml"; if (!projectFilesystem.exists(androidManifest)) { String manifestPath = this.pathToDefaultAndroidManifest.get(); String rootPrefix = "//"; Preconditions.checkState( manifestPath.startsWith(rootPrefix), "Currently, we expect this option to start with '%s', " + "indicating that it is relative to the root of the repository.", rootPrefix); manifestPath = manifestPath.substring(rootPrefix.length()); String relativePathToManifest = Paths.computeRelativePath(basePathWithSlash, manifestPath); // IntelliJ requires that the path start with a slash to indicate that it is relative to // the module. module.androidManifest = "/" + relativePathToManifest; } } // List this last so that classes from modules can shadow classes in the JDK. jdkDependency = DependentModule.newInheritedJdk(); } else { module.hasAndroidFacet = false; if (module.isIntelliJPlugin()) { jdkDependency = DependentModule.newIntelliJPluginJdk(); } else { jdkDependency = DependentModule.newStandardJdk(); } } // Assign the dependencies. module.dependencies = createDependenciesInOrder(includeSourceFolder, dependencies, jdkDependency); // Annotation processing generates sources for IntelliJ to consume, but does so outside // the module directory to avoid messing up globbing. if (projectRule instanceof JavaLibraryRule) { JavaLibraryRule javaLibraryRule = (JavaLibraryRule) projectRule; AnnotationProcessingData processingData = javaLibraryRule.getAnnotationProcessingData(); String annotationGenSrc = processingData.getGeneratedSourceFolderName(); if (annotationGenSrc != null) { module.annotationGenPath = "/" + Paths.computeRelativePath(basePathWithSlash, annotationGenSrc); module.annotationGenIsForTest = !hasSourceFoldersForSrcRule; } } return module; }
/** * @param module must be an android module * @param nameToModuleIndex * @returns the File that was written, or {@code null} if no file was written. * @throws IOException */ @VisibleForTesting @Nullable File writeProjectDotPropertiesFile(Module module, Map<String, Module> nameToModuleIndex) throws IOException { String pathToImlFile = module.pathToImlFile; SortedSet<String> references = Sets.newTreeSet(); for (DependentModule dependency : module.dependencies) { if (!dependency.isModule()) { continue; } Module dep = nameToModuleIndex.get(dependency.getModuleName()); if (dep == null) { throw new HumanReadableException( "You must define a project_config() in %s " + "containing %s. The project_config() in %s transitively depends on it.", module.target.getBuildFile(), dependency.getTargetName(), module.target.getFullyQualifiedName()); } if (!dep.isAndroidModule()) { continue; } String relativePath = Paths.computeRelativePath( Paths.getParentPath(pathToImlFile), Paths.getParentPath(dep.pathToImlFile)); // Drop the trailing slash from the path, since that's what the Android tools appear to do // when generating project.properties. if (relativePath.endsWith("/")) { relativePath = relativePath.substring(0, relativePath.length() - 1); } // This is probably a self-reference. Ignore it. if (relativePath.isEmpty()) { continue; } references.add(relativePath); } StringBuilder builder = new StringBuilder(); builder.append("# This file is automatically generated by Buck.\n"); builder.append("# Do not modify this file -- YOUR CHANGES WILL BE ERASED!\n"); // These are default values that IntelliJ or some other tool may overwrite. builder.append("target=Google Inc.:Google APIs:16\n"); builder.append("proguard.config=proguard.cfg\n"); boolean isAndroidLibrary = module.isAndroidLibrary(); if (isAndroidLibrary) { // Android does not seem to include this line for non-Android libraries. builder.append("android.library=" + isAndroidLibrary + "\n"); } int index = 1; for (String path : references) { builder.append(String.format("android.library.reference.%d=%s\n", index, path)); ++index; } final Charset charset = Charsets.US_ASCII; File outputFile = new File(createPathToProjectDotPropertiesFileFor(module)); String properties = builder.toString(); if (outputFile.exists() && Files.toString(outputFile, charset).equals(properties)) { return null; } else { Files.write(properties, outputFile, charset); return outputFile; } }