blob: 9a18a862a4b40825e9cf57acb5b53f685e911fb2 [file] [log] [blame]
#!/usr/bin/env python3
#
# Copyright 2018 - The Android Open Source Project
#
# 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.
"""It is an AIDEGen sub task : generate the project files.
This module generate IDE project files from templates.
Typical usage example:
generate_ide_project_file(project_info)
"""
import logging
import os
import pathlib
import shutil
from aidegen import constant
from aidegen.lib import common_util
from aidegen.lib.eclipse_project_file_gen import EclipseConf
# FACET_SECTION is a part of iml, which defines the framework of the project.
_FACET_SECTION = '''\
<facet type="android" name="Android">
<configuration />
</facet>'''
_SOURCE_FOLDER = (' <sourceFolder url='
'"file://%s" isTestSource="%s" />\n')
_CONTENT_URL = ' <content url="file://%s">\n'
_END_CONTENT = ' </content>\n'
_ORDER_ENTRY = (' <orderEntry type="module-library" exported="">'
'<library><CLASSES><root url="jar://%s!/" /></CLASSES>'
'<JAVADOC /><SOURCES /></library></orderEntry>\n')
_MODULE_ORDER_ENTRY = (' <orderEntry type="module" '
'module-name="%s" />')
_MODULE_SECTION = (' <module fileurl="file:///$PROJECT_DIR$/%s.iml"'
' filepath="$PROJECT_DIR$/%s.iml" />')
_SUB_MODULES_SECTION = (' <module fileurl="file:///%s" '
'filepath="%s" />')
_VCS_SECTION = ' <mapping directory="%s" vcs="Git" />'
_FACET_TOKEN = '@FACETS@'
_SOURCE_TOKEN = '@SOURCES@'
_MODULE_DEP_TOKEN = '@MODULE_DEPENDENCIES@'
_MODULE_TOKEN = '@MODULES@'
_VCS_TOKEN = '@VCS@'
_JAVA_FILE_PATTERN = '%s/*.java'
_ROOT_DIR = constant.AIDEGEN_ROOT_PATH
_IDEA_DIR = os.path.join(_ROOT_DIR, 'templates/idea')
_TEMPLATE_IML_PATH = os.path.join(_ROOT_DIR, 'templates/module-template.iml')
_IDEA_FOLDER = '.idea'
_MODULES_XML = 'modules.xml'
_VCS_XML = 'vcs.xml'
_TEMPLATE_MODULES_PATH = os.path.join(_IDEA_DIR, _MODULES_XML)
_TEMPLATE_VCS_PATH = os.path.join(_IDEA_DIR, _VCS_XML)
_DEPENDENCIES_IML = 'dependencies.iml'
_COPYRIGHT_FOLDER = 'copyright'
_CODE_STYLE_FOLDER = 'codeStyles'
_COMPILE_XML = 'compiler.xml'
_MISC_XML = 'misc.xml'
_ANDROID_MANIFEST = 'AndroidManifest.xml'
_IML_EXTENSION = '.iml'
_FRAMEWORK_JAR = os.sep + 'framework.jar'
_HIGH_PRIORITY_JARS = [_FRAMEWORK_JAR]
_GIT_FOLDER_NAME = '.git'
# Support gitignore by symbolic link to aidegen/data/gitignore_template.
_GITIGNORE_FILE_NAME = '.gitignore'
_GITIGNORE_REL_PATH = 'tools/asuite/aidegen/data/gitignore_template'
_GITIGNORE_ABS_PATH = os.path.join(constant.ANDROID_ROOT_PATH,
_GITIGNORE_REL_PATH)
# Support code style by symbolic link to aidegen/data/AndroidStyle_aidegen.xml.
_CODE_STYLE_REL_PATH = 'tools/asuite/aidegen/data/AndroidStyle_aidegen.xml'
_CODE_STYLE_SRC_PATH = os.path.join(constant.ANDROID_ROOT_PATH,
_CODE_STYLE_REL_PATH)
_ECLIP_SRC_ENTRY = '<classpathentry exported="true" kind="src" path="{}"/>\n'
_ECLIP_LIB_ENTRY = '<classpathentry exported="true" kind="lib" path="{}"/>\n'
_ECLIP_TEMPLATE_PATH = os.path.join(_ROOT_DIR, 'templates/eclipse/eclipse.xml')
_ECLIP_EXTENSION = '.classpath'
_ECLIP_SRC_TOKEN = '@SRC@'
_ECLIP_LIB_TOKEN = '@LIB@'
# b/121256503: Prevent duplicated iml names from breaking IDEA.
# Use a map to cache in-using(already used) iml project file names.
_USED_NAME_CACHE = dict()
def get_unique_iml_name(abs_module_path):
"""Create a unique iml name if needed.
If the name of last sub folder is used already, prefixing it with prior sub
folder names as a candidate name. If finally, it's unique, storing in
_USED_NAME_CACHE as: { abs_module_path:unique_name }. The cts case and UX of
IDE view are the main reasons why using module path strategy but not name of
module directly. Following is the detailed strategy:
1. While loop composes a sensible and shorter name, by checking unique to
finish the loop and finally add to cache.
Take ['cts', 'tests', 'app', 'ui'] an example, if 'ui' isn't occupied,
use it, else try 'cts_ui', then 'cts_app_ui', the worst case is whole
three candidate names are occupied already.
2. 'Else' for that while stands for no suitable name generated, so trying
'cts_tests_app_ui' directly. If it's still non unique, e.g., module path
cts/xxx/tests/app/ui occupied that name already, appending increasing
sequence number to get a unique name.
Args:
abs_module_path: Full module path string.
Return:
String: A unique iml name.
"""
if abs_module_path in _USED_NAME_CACHE:
return _USED_NAME_CACHE[abs_module_path]
uniq_name = abs_module_path.strip(os.sep).split(os.sep)[-1]
if any(uniq_name == name for name in _USED_NAME_CACHE.values()):
parent_path = os.path.relpath(abs_module_path,
constant.ANDROID_ROOT_PATH)
sub_folders = parent_path.split(os.sep)
zero_base_index = len(sub_folders) - 1
# Start compose a sensible, shorter and unique name.
while zero_base_index > 0:
uniq_name = '_'.join(
[sub_folders[0], '_'.join(sub_folders[zero_base_index:])])
zero_base_index = zero_base_index - 1
if uniq_name not in _USED_NAME_CACHE.values():
break
else:
# b/133393638: To handle several corner cases.
uniq_name_base = parent_path.strip(os.sep).replace(os.sep, '_')
i = 0
uniq_name = uniq_name_base
while uniq_name in _USED_NAME_CACHE.values():
i = i + 1
uniq_name = '_'.join([uniq_name_base, str(i)])
_USED_NAME_CACHE[abs_module_path] = uniq_name
logging.debug('Unique name for module path of %s is %s.', abs_module_path,
uniq_name)
return uniq_name
def _generate_intellij_project_file(project_info, iml_path_list=None):
"""Generates IntelliJ project file.
Args:
project_info: ProjectInfo instance.
iml_path_list: An optional list of submodule's iml paths, default None.
"""
source_dict = dict.fromkeys(
list(project_info.source_path['source_folder_path']), False)
source_dict.update(
dict.fromkeys(list(project_info.source_path['test_folder_path']), True))
project_info.iml_path, _ = _generate_iml(
constant.ANDROID_ROOT_PATH, project_info.project_absolute_path,
source_dict, list(project_info.source_path['jar_path']),
project_info.project_relative_path)
_generate_modules_xml(project_info.project_absolute_path, iml_path_list)
project_info.git_path = _generate_vcs_xml(
project_info.project_absolute_path)
_copy_constant_project_files(project_info.project_absolute_path)
def generate_ide_project_files(projects):
"""Generate IDE project files by a list of ProjectInfo instances.
For multiple modules case, we call _generate_intellij_project_file to
generate iml file for submodules first and pass submodules' iml file paths
as an argument to function _generate_intellij_project_file when we generate
main module.iml file. In this way, we can add submodules' dependencies iml
and their own iml file paths to main module's module.xml.
Args:
projects: A list of ProjectInfo instances.
"""
if projects[0].config.ide_name == constant.IDE_ECLIPSE:
generate_eclipse_project_files(projects)
else:
# Initialization
_USED_NAME_CACHE.clear()
for project in projects[1:]:
_generate_intellij_project_file(project)
iml_paths = [project.iml_path for project in projects[1:]]
_generate_intellij_project_file(projects[0], iml_paths)
_merge_project_vcs_xmls(projects)
def _generate_eclipse_project_file(project_info):
"""Generates Eclipse project files.
Args:
project_info: ProjectInfo instance.
"""
eclipse_configure = EclipseConf(project_info)
eclipse_configure.generate_project_file()
source_dict = dict.fromkeys(
list(project_info.source_path['source_folder_path']), False)
source_dict.update(
dict.fromkeys(list(project_info.source_path['test_folder_path']), True))
source_dict.update(
dict.fromkeys(list(project_info.source_path['r_java_path']), True))
project_info.iml_path = _generate_classpath(
project_info.project_absolute_path, list(sorted(source_dict)),
list(project_info.source_path['jar_path']))
def generate_eclipse_project_files(projects):
"""Generate Eclipse project files by a list of ProjectInfo instances.
Args:
projects: A list of ProjectInfo instances.
"""
for project in projects:
_generate_eclipse_project_file(project)
def _copy_constant_project_files(target_path):
"""Copy project files to target path with error handling.
This function would copy compiler.xml, misc.xml, codeStyles folder and
copyright folder to target folder. Since these files aren't mandatory in
IntelliJ, it only logs when an IOError occurred.
Args:
target_path: A folder path to copy content to.
"""
try:
_copy_to_idea_folder(target_path, _COPYRIGHT_FOLDER)
_copy_to_idea_folder(target_path, _CODE_STYLE_FOLDER)
code_style_target_path = os.path.join(target_path, _IDEA_FOLDER,
_CODE_STYLE_FOLDER, 'Project.xml')
# Base on current working directory to prepare the relevant location
# of the symbolic link file, and base on the symlink file location to
# prepare the relevant code style source path.
rel_target = os.path.relpath(code_style_target_path, os.getcwd())
rel_source = os.path.relpath(_CODE_STYLE_SRC_PATH,
os.path.dirname(code_style_target_path))
logging.debug('Relative target symlink path: %s.', rel_target)
logging.debug('Relative code style source path: %s.', rel_source)
os.symlink(rel_source, rel_target)
# Create .gitignore if it doesn't exist.
_generate_git_ignore(target_path)
shutil.copy(
os.path.join(_IDEA_DIR, _COMPILE_XML),
os.path.join(target_path, _IDEA_FOLDER, _COMPILE_XML))
shutil.copy(
os.path.join(_IDEA_DIR, _MISC_XML),
os.path.join(target_path, _IDEA_FOLDER, _MISC_XML))
except IOError as err:
logging.warning('%s can\'t copy the project files\n %s', target_path,
err)
def _copy_to_idea_folder(target_path, folder_name):
"""Copy folder to project .idea path.
Args:
target_path: Path of target folder.
folder_name: Name of target folder.
"""
target_folder_path = os.path.join(target_path, _IDEA_FOLDER, folder_name)
# Existing folder needs to be removed first, otherwise it will raise
# IOError.
if os.path.exists(target_folder_path):
shutil.rmtree(target_folder_path)
shutil.copytree(os.path.join(_IDEA_DIR, folder_name), target_folder_path)
def _handle_facet(content, path):
"""Handle facet part of iml.
If the module is an Android app, which contains AndroidManifest.xml, it
should have a facet of android, otherwise we don't need facet in iml.
Args:
content: String content of iml.
path: Path of the module.
Returns:
String: Content with facet handled.
"""
facet = ''
if os.path.isfile(os.path.join(path, _ANDROID_MANIFEST)):
facet = _FACET_SECTION
return content.replace(_FACET_TOKEN, facet)
def _handle_module_dependency(root_path, content, jar_dependencies):
"""Handle module dependency part of iml.
Args:
root_path: Android source tree root path.
content: String content of iml.
jar_dependencies: List of the jar path.
Returns:
String: Content with module dependency handled.
"""
module_library = ''
dependencies = []
# Reorder deps in the iml generated by IntelliJ by inserting priority jars.
for jar_path in jar_dependencies:
if any((jar_path.endswith(high_priority_jar))
for high_priority_jar in _HIGH_PRIORITY_JARS):
module_library += _ORDER_ENTRY % os.path.join(root_path, jar_path)
else:
dependencies.append(jar_path)
# IntelliJ indexes jars as dependencies from iml by the ascending order.
# Without sorting, the order of jar list changes everytime. Sort the jar
# list to keep the jar dependencies in consistency. It also can help us to
# discover potential issues like duplicated classes.
for jar_path in sorted(dependencies):
module_library += _ORDER_ENTRY % os.path.join(root_path, jar_path)
return content.replace(_MODULE_DEP_TOKEN, module_library)
def _is_project_relative_source(source, relative_path):
"""Check if the relative path of a file is a source relative path.
Check if the file path starts with the relative path or the relative is an
Android source tree root path.
Args:
source: The file path to be checked.
relative_path: The relative path to be checked.
Returns:
True if the file is a source relative path, otherwise False.
"""
abs_path = common_util.get_abs_path(relative_path)
if common_util.is_android_root(abs_path):
return True
if _is_source_under_relative_path(source, relative_path):
return True
return False
def _handle_source_folder(root_path, content, source_dict, is_module,
relative_path):
"""Handle source folder part of iml.
It would make the source folder group by content.
e.g.
<content url="file://$MODULE_DIR$/a">
<sourceFolder url="file://$MODULE_DIR$/a/b" isTestSource="False" />
<sourceFolder url="file://$MODULE_DIR$/a/test" isTestSource="True" />
<sourceFolder url="file://$MODULE_DIR$/a/d/e" isTestSource="False" />
</content>
Args:
root_path: Android source tree root path.
content: String content of iml.
source_dict: A dictionary of sources path with a flag to identify the
path is test or source folder in IntelliJ.
e.g.
{'path_a': True, 'path_b': False}
is_module: True if it is module iml, otherwise it is dependencies iml.
relative_path: Relative path of the module.
Returns:
String: Content with source folder handled.
"""
source_list = list(source_dict.keys())
source_list.sort()
src_builder = []
if is_module:
# Set the content url to module's path since it's the iml of target
# project which only has it's sub-folders in source_list.
src_builder.append(
_CONTENT_URL % os.path.join(root_path, relative_path))
for path, is_test_flag in sorted(source_dict.items()):
if _is_project_relative_source(path, relative_path):
src_builder.append(_SOURCE_FOLDER % (os.path.join(
root_path, path), is_test_flag))
src_builder.append(_END_CONTENT)
else:
for path, is_test_flag in sorted(source_dict.items()):
path = os.path.join(root_path, path)
src_builder.append(_CONTENT_URL % path)
src_builder.append(_SOURCE_FOLDER % (path, is_test_flag))
src_builder.append(_END_CONTENT)
return content.replace(_SOURCE_TOKEN, ''.join(src_builder))
def _trim_same_root_source(source_list):
"""Trim the source which has the same root.
The source list may contain lots of duplicate sources.
For example:
a/b, a/b/c, a/b/d
We only need to import a/b in iml, this function is used to trim redundant
sources.
Args:
source_list: Sorted list of the sources.
Returns:
List: The trimmed source list.
"""
tmp_source_list = [source_list[0]]
for src_path in source_list:
if ''.join([tmp_source_list[-1],
os.sep]) not in ''.join([src_path, os.sep]):
tmp_source_list.append(src_path)
return sorted(tmp_source_list)
def _is_source_under_relative_path(source, relative_path):
"""Check if a source file is a project relative path file.
Args:
source: Android source file path.
relative_path: Relative path of the module.
Returns:
True if source file is a project relative path file, otherwise False.
"""
return source == relative_path or source.startswith(relative_path + os.sep)
# pylint: disable=too-many-locals
def _generate_iml(root_path, module_path, source_dict, jar_dependencies,
relative_path):
"""Generate iml file.
Args:
root_path: Android source tree root path.
module_path: Absolute path of the module.
source_dict: A dictionary of sources path with a flag to distinguish the
path is test or source folder in IntelliJ.
e.g.
{'path_a': True, 'path_b': False}
jar_dependencies: List of the jar path.
relative_path: Relative path of the module.
Returns:
String: The absolute paths of module iml and dependencies iml.
"""
template = common_util.read_file_content(_TEMPLATE_IML_PATH)
# Separate module and dependencies source folder
project_source_dict = {}
for source in list(source_dict):
if _is_project_relative_source(source, relative_path):
is_test = source_dict.get(source)
source_dict.pop(source)
project_source_dict.update({source: is_test})
# Generate module iml.
module_content = _handle_facet(template, module_path)
module_content = _handle_source_folder(
root_path, module_content, project_source_dict, True, relative_path)
# b/121256503: Prevent duplicated iml names from breaking IDEA.
module_name = get_unique_iml_name(module_path)
module_iml_path = os.path.join(module_path, module_name + _IML_EXTENSION)
dep_name = _get_dependencies_name(module_name)
dep_sect = _MODULE_ORDER_ENTRY % dep_name
module_content = module_content.replace(_MODULE_DEP_TOKEN, dep_sect)
common_util.file_generate(module_iml_path, module_content)
# Generate dependencies iml.
dependencies_content = template.replace(_FACET_TOKEN, '')
dependencies_content = _handle_source_folder(
root_path, dependencies_content, source_dict, False, relative_path)
dependencies_content = _handle_module_dependency(
root_path, dependencies_content, jar_dependencies)
dependencies_iml_path = os.path.join(module_path, dep_name + _IML_EXTENSION)
common_util.file_generate(dependencies_iml_path, dependencies_content)
logging.debug('Paired iml names are %s, %s', module_iml_path,
dependencies_iml_path)
# The dependencies_iml_path is use for removing the file itself in unittest.
return module_iml_path, dependencies_iml_path
def _generate_classpath(module_path, source_list, jar_dependencies):
"""Generate .classpath file.
Args:
module_path: Absolute path of the module.
source_list: A list of sources path.
jar_dependencies: List of the jar path.
Returns:
String: The absolute paths of .classpath.
"""
template = common_util.read_file_content(_ECLIP_TEMPLATE_PATH)
src_list = [_ECLIP_SRC_ENTRY.format(s) for s in source_list]
template = template.replace(_ECLIP_SRC_TOKEN, ''.join(src_list))
lib_list = [_ECLIP_LIB_ENTRY.format(j) for j in jar_dependencies]
template = template.replace(_ECLIP_LIB_TOKEN, ''.join(lib_list))
classpath_path = os.path.join(module_path, _ECLIP_EXTENSION)
common_util.file_generate(classpath_path, template)
return classpath_path
def _get_dependencies_name(module_name):
"""Get module's dependencies iml name which will be written in module.xml.
Args:
module_name: The name will be appended to "dependencies-".
Returns:
String: The joined dependencies iml file name, e.g. "dependencies-core"
"""
return '-'.join([constant.KEY_DEPENDENCIES, module_name])
def _generate_modules_xml(module_path, iml_path_list=None):
"""Generate modules.xml file.
IntelliJ uses modules.xml to import which modules should be loaded to
project. Only in multiple modules case will we pass iml_path_list of
submodules' dependencies and their iml file paths to add them into main
module's module.xml file. The dependencies iml file names will be changed
from original dependencies.iml to dependencies-[module_name].iml,
e.g. dependencies-core.iml for core.iml.
Args:
module_path: Path of the module.
iml_path_list: A list of submodule iml paths.
"""
content = common_util.read_file_content(_TEMPLATE_MODULES_PATH)
# b/121256503: Prevent duplicated iml names from breaking IDEA.
module_name = get_unique_iml_name(module_path)
file_name = os.path.splitext(module_name)[0]
dep_name = _get_dependencies_name(file_name)
module_list = [
_MODULE_SECTION % (module_name, module_name),
_MODULE_SECTION % (dep_name, dep_name)
]
if iml_path_list:
for iml_path in iml_path_list:
iml_dir, iml_name = os.path.split(iml_path)
dep_file = _get_dependencies_name(iml_name)
dep_path = os.path.join(iml_dir, dep_file)
module_list.append(_SUB_MODULES_SECTION % (dep_path, dep_path))
module_list.append(_SUB_MODULES_SECTION % (iml_path, iml_path))
module = '\n'.join(module_list)
content = content.replace(_MODULE_TOKEN, module)
target_path = os.path.join(module_path, _IDEA_FOLDER, _MODULES_XML)
common_util.file_generate(target_path, content)
def _generate_vcs_xml(module_path):
"""Generate vcs.xml file.
IntelliJ use vcs.xml to record version control software's information.
Since we are using a single project file, it will only contain the
module itself. If there is no git folder inside, it would find it in
parent's folder.
Args:
module_path: Path of the module.
Return:
String: A module's git path.
"""
git_path = module_path
while not os.path.isdir(os.path.join(git_path, _GIT_FOLDER_NAME)):
git_path = str(pathlib.Path(git_path).parent)
if git_path == os.sep:
logging.warning('%s can\'t find its .git folder', module_path)
return None
_write_vcs_xml(module_path, [git_path])
return git_path
def _write_vcs_xml(module_path, git_paths):
"""Write the git path into vcs.xml.
For main module, the vcs.xml should include all modules' git path.
For submodules, there is only one git path in vcs.xml.
Args:
module_path: Path of the module.
git_paths: A list of git path.
"""
_vcs_content = '\n'.join([_VCS_SECTION % p for p in git_paths if p])
content = common_util.read_file_content(_TEMPLATE_VCS_PATH)
content = content.replace(_VCS_TOKEN, _vcs_content)
target_path = os.path.join(module_path, _IDEA_FOLDER, _VCS_XML)
common_util.file_generate(target_path, content)
def _merge_project_vcs_xmls(projects):
"""Merge sub projects' git paths into main project's vcs.xml.
After all projects' vcs.xml are generated, collect the git path of each
projects and write them into main project's vcs.xml.
Args:
projects: A list of ProjectInfo instances.
"""
main_project_absolute_path = projects[0].project_absolute_path
git_paths = [project.git_path for project in projects]
_write_vcs_xml(main_project_absolute_path, git_paths)
def _generate_git_ignore(target_folder):
"""Generate .gitignore file.
In target_folder, if there's no .gitignore file, uses symlink() to generate
one to hide project content files from git.
Args:
target_folder: An absolute path string of target folder.
"""
# TODO(b/133639849): Provide a common method to create symbolic link.
# TODO(b/133641803): Move out aidegen artifacts from Android repo.
try:
gitignore_abs_path = os.path.join(target_folder, _GITIGNORE_FILE_NAME)
rel_target = os.path.relpath(gitignore_abs_path, os.getcwd())
rel_source = os.path.relpath(_GITIGNORE_ABS_PATH, target_folder)
logging.debug('Relative target symlink path: %s.', rel_target)
logging.debug('Relative ignore_template source path: %s.', rel_source)
if not os.path.exists(gitignore_abs_path):
os.symlink(rel_source, rel_target)
except OSError as err:
logging.error('Not support to run aidegen on Windows.\n %s', err)