blob: 8dd6d73eec66f60e18e8d9ec1a647981fb99e87c [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.
"""AIDEgen
This CLI generates project files for using in IntelliJ, such as:
- iml
- .idea/compiler.xml
- .idea/misc.xml
- .idea/modules.xml
- .idea/vcs.xml
- .idea/.name
- .idea/copyright/Apache_2.xml
- .idea/copyright/progiles_settings.xml
- Sample usage:
- Change directory to AOSP root first.
$ cd /user/home/aosp/
- Generating project files under packages/apps/Settings folder.
$ aidegen packages/apps/Settings
or
$ aidegen Settings
or
$ cd packages/apps/Settings;aidegen
"""
from __future__ import absolute_import
import argparse
import os
import sys
import traceback
from pathlib import Path
from aidegen import constant
from aidegen.lib import aidegen_metrics
from aidegen.lib import common_util
from aidegen.lib import eclipse_project_file_gen
from aidegen.lib import errors
from aidegen.lib import ide_util
from aidegen.lib import module_info
from aidegen.lib import native_module_info
from aidegen.lib import native_project_info
from aidegen.lib import native_util
from aidegen.lib import project_config
from aidegen.lib import project_file_gen
from aidegen.lib import project_info
from aidegen.vscode import vscode_native_project_file_gen
from aidegen.vscode import vscode_workspace_file_gen
AIDEGEN_REPORT_LINK = ('To report an AIDEGen tool problem, please use this '
'link: https://goto.google.com/aidegen-bug')
_CONGRATULATIONS = common_util.COLORED_PASS('CONGRATULATIONS:')
_LAUNCH_SUCCESS_MSG = (
'IDE launched successfully. Please check your IDE window.')
_LAUNCH_ECLIPSE_SUCCESS_MSG = (
'The project files .classpath and .project are generated under '
'{PROJECT_PATH} and AIDEGen doesn\'t import the project automatically, '
'please import the project manually by steps: File -> Import -> select \''
'General\' -> \'Existing Projects into Workspace\' -> click \'Next\' -> '
'Choose the root directory -> click \'Finish\'.')
_IDE_CACHE_REMINDER_MSG = (
'To prevent the existed IDE cache from impacting your IDE dependency '
'analysis, please consider to clear IDE caches if necessary. To do that, in'
' IntelliJ IDEA, go to [File > Invalidate Caches / Restart...].')
_MAX_TIME = 1
_SKIP_BUILD_INFO_FUTURE = ''.join([
'AIDEGen build time exceeds {} minute(s).\n'.format(_MAX_TIME),
project_config.SKIP_BUILD_INFO.rstrip('.'), ' in the future.'
])
_INFO = common_util.COLORED_INFO('INFO:')
_SKIP_MSG = _SKIP_BUILD_INFO_FUTURE.format(
common_util.COLORED_INFO('aidegen [ module(s) ] -s'))
_TIME_EXCEED_MSG = '\n{} {}\n'.format(_INFO, _SKIP_MSG)
_LAUNCH_CLION_IDES = [
constant.IDE_CLION, constant.IDE_INTELLIJ, constant.IDE_ECLIPSE]
_CHOOSE_LANGUAGE_MSG = ('The scope of your modules contains {} different '
'languages as follows:\n{}\nPlease select the one you '
'would like to implement.\t')
_LANGUAGE_OPTIONS = [constant.JAVA, constant.C_CPP]
_NO_ANY_PROJECT_EXIST = 'There is no Java, C/C++ or Rust target.'
_NO_LANGUAGE_PROJECT_EXIST = 'There is no {} target.'
_NO_IDE_LAUNCH_PATH = 'Can not find the IDE path : {}'
def _parse_args(args):
"""Parse command line arguments.
Args:
args: A list of arguments.
Returns:
An argparse.Namespace class instance holding parsed args.
"""
parser = argparse.ArgumentParser(
description=__doc__,
formatter_class=argparse.RawDescriptionHelpFormatter,
usage=('aidegen [module_name1 module_name2... '
'project_path1 project_path2...]'))
parser.required = False
parser.add_argument(
'targets',
type=str,
nargs='*',
default=[''],
help=('Android module name or path.'
'e.g. Settings or packages/apps/Settings'))
parser.add_argument(
'-d',
'--depth',
type=int,
choices=range(10),
default=0,
help='The depth of module referenced by source.')
parser.add_argument(
'-v',
'--verbose',
action='store_true',
help='Display DEBUG level logging.')
parser.add_argument(
'-i',
'--ide',
default=['u'],
help=('Launch IDE type, j: IntelliJ, s: Android Studio, e: Eclipse, '
'c: CLion, v: VS Code. The default value is \'u\': undefined.'))
parser.add_argument(
'-p',
'--ide-path',
dest='ide_installed_path',
help='IDE installed path.')
parser.add_argument(
'-n', '--no_launch', action='store_true', help='Do not launch IDE.')
parser.add_argument(
'-r',
'--config-reset',
dest='config_reset',
action='store_true',
help='Reset all saved configurations, e.g., preferred IDE version.')
parser.add_argument(
'-s',
'--skip-build',
dest='skip_build',
action='store_true',
help=('Skip building jars or modules that create java files in build '
'time, e.g. R/AIDL/Logtags.'))
parser.add_argument(
'-a',
'--android-tree',
dest='android_tree',
action='store_true',
help='Generate whole Android source tree project file for IDE.')
parser.add_argument(
'-e',
'--exclude-paths',
dest='exclude_paths',
nargs='*',
help='Exclude the directories in IDE.')
parser.add_argument(
'-V',
'--version',
action='store_true',
help='Print aidegen version string.')
parser.add_argument(
'-l',
'--language',
default=['u'],
help=('Launch IDE with a specific language, j: Java, c: C/C++, r: '
'Rust. The default value is \'u\': undefined.'))
return parser.parse_args(args)
def _generate_project_files(projects):
"""Generate project files by IDE type.
Args:
projects: A list of ProjectInfo instances.
"""
config = project_config.ProjectConfig.get_instance()
if config.ide_name == constant.IDE_ECLIPSE:
eclipse_project_file_gen.EclipseConf.generate_ide_project_files(
projects)
else:
project_file_gen.ProjectFileGenerator.generate_ide_project_files(
projects)
def _launch_ide(ide_util_obj, project_absolute_path):
"""Launch IDE through ide_util instance.
To launch IDE,
1. Set IDE config.
2. For IntelliJ, use .idea as open target is better than .iml file,
because open the latter is like to open a kind of normal file.
3. Show _LAUNCH_SUCCESS_MSG to remind users IDE being launched.
Args:
ide_util_obj: An ide_util instance.
project_absolute_path: A string of project absolute path.
"""
ide_util_obj.config_ide(project_absolute_path)
if ide_util_obj.ide_name() == constant.IDE_ECLIPSE:
launch_msg = ' '.join([_LAUNCH_SUCCESS_MSG,
_LAUNCH_ECLIPSE_SUCCESS_MSG.format(
PROJECT_PATH=project_absolute_path)])
else:
launch_msg = _LAUNCH_SUCCESS_MSG
print('\n{} {}\n'.format(_CONGRATULATIONS, launch_msg))
print('\n{} {}\n'.format(_INFO, _IDE_CACHE_REMINDER_MSG))
# Send the end message to Clearcut server before launching IDE to make sure
# the execution time is correct.
aidegen_metrics.ends_asuite_metrics(constant.EXIT_CODE_EXCEPTION)
ide_util_obj.launch_ide()
def _launch_native_projects(ide_util_obj, args, cmakelists):
"""Launches C/C++ projects with IDE.
AIDEGen provides the IDE argument for CLion, but there's still a implicit
way to launch it. The rules to launch it are:
1. If no target IDE, we don't have to launch any IDE for C/C++ project.
2. If the target IDE is IntelliJ or Eclipse, we should launch C/C++
projects with CLion.
Args:
ide_util_obj: An ide_util instance.
args: An argparse.Namespace class instance holding parsed args.
cmakelists: A list of CMakeLists.txt file paths.
"""
if not ide_util_obj:
return
native_ide_util_obj = ide_util_obj
ide_name = constant.IDE_NAME_DICT[args.ide[0]]
if ide_name in _LAUNCH_CLION_IDES:
native_ide_util_obj = ide_util.get_ide_util_instance('c')
if native_ide_util_obj:
_launch_ide(native_ide_util_obj, ' '.join(cmakelists))
def _create_and_launch_java_projects(ide_util_obj, targets):
"""Launches Android of Java(Kotlin) projects with IDE.
Args:
ide_util_obj: An ide_util instance.
targets: A list of build targets.
"""
projects = project_info.ProjectInfo.generate_projects(targets)
project_info.ProjectInfo.multi_projects_locate_source(projects)
_generate_project_files(projects)
if ide_util_obj:
_launch_ide(ide_util_obj, projects[0].project_absolute_path)
def _launch_ide_by_module_contents(args, ide_util_obj, language,
lang_targets, all_langs=False):
"""Deals with the suitable IDE launch action.
The rules of AIDEGen launching IDE with languages are:
1. If no IDE or language is specific, the priority of the language is:
a) Java
aidegen frameworks/base
launch Java projects of frameworks/base in IntelliJ.
b) C/C++
aidegen hardware/interfaces/vibrator/aidl/default
launch C/C++ project of hardware/interfaces/vibrator/aidl/default
in CLion.
c) Rust
aidegen external/rust/crates/protobuf
launch Rust project of external/rust/crates/protobuf in VS Code.
2. If the IDE is specific, launch related projects in the IDE.
a) aidegen frameworks/base -i j
launch Java projects of frameworks/base in IntelliJ.
aidegen frameworks/base -i s
launch Java projects of frameworks/base in Android Studio.
aidegen frameworks/base -i e
launch Java projects of frameworks/base in Eclipse.
b) aidegen frameworks/base -i c
launch C/C++ projects of frameworks/base in CLion.
c) aidegen external/rust/crates/protobuf -i v
launch Rust project of external/rust/crates/protobuf in VS Code.
3. If the launguage is specific, launch relative language projects in the
relative IDE.
a) aidegen frameworks/base -l j
launch Java projects of frameworks/base in IntelliJ.
b) aidegen frameworks/base -l c
launch C/C++ projects of frameworks/base in CLion.
c) aidegen external/rust/crates/protobuf -l r
launch Rust projects of external/rust/crates/protobuf in VS Code.
4. Both of the IDE and language are specific, launch the IDE with the
relative language projects. If the IDE conflicts with the language, the
IDE is prior to the language.
a) aidegen frameworks/base -i j -l j
launch Java projects of frameworks/base in IntelliJ.
b) aidegen frameworks/base -i s -l c
launch C/C++ projects of frameworks/base in Android Studio.
c) aidegen frameworks/base -i c -l j
launch C/C++ projects of frameworks/base in CLion.
Args:
args: A list of system arguments.
ide_util_obj: An ide_util instance.
language: A string of the language to be edited in the IDE.
lang_targets: A dict contains None or list of targets of different
languages. E.g.
{
'Java': ['modules1', 'modules2'],
'C/C++': ['modules3'],
'Rust': None,
...
}
all_langs: A boolean, True to launch all languages else False.
"""
if lang_targets is None:
print(constant.WARN_MSG.format(
common_util.COLORED_INFO('Warning:'), _NO_ANY_PROJECT_EXIST))
return
targets = lang_targets
java_list = targets[constant.JAVA] if constant.JAVA in targets else None
c_cpp_list = targets[constant.C_CPP] if constant.C_CPP in targets else None
rust_list = targets[constant.RUST] if constant.RUST in targets else None
if all_langs:
_launch_vscode(ide_util_obj, project_info.ProjectInfo.modules_info,
java_list, c_cpp_list, rust_list)
return
if not (java_list or c_cpp_list or rust_list):
print(constant.WARN_MSG.format(
common_util.COLORED_INFO('Warning:'), _NO_ANY_PROJECT_EXIST))
return
if language == constant.JAVA:
if not java_list:
print(constant.WARN_MSG.format(
common_util.COLORED_INFO('Warning:'),
_NO_LANGUAGE_PROJECT_EXIST.format(constant.JAVA)))
return
_create_and_launch_java_projects(ide_util_obj, java_list)
return
if language == constant.C_CPP:
if not c_cpp_list:
print(constant.WARN_MSG.format(
common_util.COLORED_INFO('Warning:'),
_NO_LANGUAGE_PROJECT_EXIST.format(constant.C_CPP)))
return
native_project_info.NativeProjectInfo.generate_projects(c_cpp_list)
native_project_file = native_util.generate_clion_projects(c_cpp_list)
if native_project_file:
_launch_native_projects(ide_util_obj, args, [native_project_file])
def _launch_vscode(ide_util_obj, atest_module_info, jtargets, ctargets,
rtargets):
"""Launches targets with VSCode IDE.
Args:
ide_util_obj: An ide_util instance.
atest_module_info: A ModuleInfo instance contains the data of
module-info.json.
jtargets: A list of Java project targets.
ctargets: A list of C/C++ project targets.
rtargets: A list of Rust project targets.
"""
abs_paths = []
if jtargets:
abs_paths.extend(_get_java_project_paths(jtargets, atest_module_info))
if ctargets:
abs_paths.extend(_get_cc_project_paths(ctargets))
if rtargets:
root_dir = common_util.get_android_root_dir()
abs_paths.extend(_get_rust_project_paths(rtargets, root_dir))
if not (jtargets or ctargets or rtargets):
print(constant.WARN_MSG.format(
common_util.COLORED_INFO('Warning:'), _NO_ANY_PROJECT_EXIST))
return
vs_path = vscode_workspace_file_gen.generate_code_workspace_file(abs_paths)
if not ide_util_obj:
return
_launch_ide(ide_util_obj, vs_path)
def _get_java_project_paths(jtargets, atest_module_info):
"""Gets the Java absolute project paths from the input Java targets.
Args:
jtargets: A list of strings of Java targets.
atest_module_info: A ModuleInfo instance contains the data of
module-info.json.
Returns:
A list of the Java absolute project paths.
"""
abs_paths = []
for target in jtargets:
_, abs_path = common_util.get_related_paths(atest_module_info, target)
if abs_path:
abs_paths.append(abs_path)
return abs_paths
def _get_cc_project_paths(ctargets):
"""Gets the C/C++ absolute project paths from the input C/C++ targets.
Args:
ctargets: A list of strings of C/C++ targets.
Returns:
A list of the C/C++ absolute project paths.
"""
abs_paths = []
cc_module_info = native_module_info.NativeModuleInfo()
native_project_info.NativeProjectInfo.generate_projects(ctargets)
vs_gen = vscode_native_project_file_gen.VSCodeNativeProjectFileGenerator
for target in ctargets:
_, abs_path = common_util.get_related_paths(cc_module_info, target)
if not abs_path:
continue
vs_native = vs_gen(abs_path)
vs_native.generate_c_cpp_properties_json_file()
if abs_path not in abs_paths:
abs_paths.append(abs_path)
return abs_paths
def _get_rust_project_paths(rtargets, root_dir):
"""Gets the Rust absolute project paths from the input Rust targets.
Args:
rtargets: A list of strings of Rust targets.
root_dir: A string of the Android root directory.
Returns:
A list of the Rust absolute project paths.
"""
abs_paths = []
for rtarget in rtargets:
path = rtarget
# If rtarget is not an absolute path, make it an absolute one.
if not common_util.is_source_under_relative_path(rtarget, root_dir):
path = os.path.join(root_dir, rtarget)
abs_paths.append(path)
return abs_paths
@common_util.time_logged(message=_TIME_EXCEED_MSG, maximum=_MAX_TIME)
def main_with_message(args):
"""Main entry with skip build message.
Args:
args: A list of system arguments.
"""
aidegen_main(args)
@common_util.time_logged
def main_without_message(args):
"""Main entry without skip build message.
Args:
args: A list of system arguments.
"""
aidegen_main(args)
# pylint: disable=broad-except
def main(argv):
"""Main entry.
Show skip build message in aidegen main process if users command skip_build
otherwise remind them to use it and include metrics supports.
Args:
argv: A list of system arguments.
"""
exit_code = constant.EXIT_CODE_NORMAL
launch_ide = True
ask_version = False
try:
args = _parse_args(argv)
# If the targets is the default value, sets it to the absolute path to
# avoid the issues caused by the empty path.
if args.targets == ['']:
args.targets = [os.path.abspath(os.getcwd())]
if args.version:
ask_version = True
version_file = os.path.join(os.path.dirname(__file__),
constant.VERSION_FILE)
print(common_util.read_file_content(version_file))
sys.exit(constant.EXIT_CODE_NORMAL)
if args.ide_installed_path:
if not Path(args.ide_installed_path).exists():
print(_NO_IDE_LAUNCH_PATH.format(args.ide_installed_path))
sys.exit(constant.EXIT_CODE_NORMAL)
launch_ide = not args.no_launch
common_util.configure_logging(args.verbose)
is_whole_android_tree = project_config.is_whole_android_tree(
args.targets, args.android_tree)
references = [constant.ANDROID_TREE] if is_whole_android_tree else []
aidegen_metrics.starts_asuite_metrics(references)
if args.skip_build:
main_without_message(args)
else:
main_with_message(args)
except BaseException as err:
exit_code = constant.EXIT_CODE_EXCEPTION
_, exc_value, exc_traceback = sys.exc_info()
if isinstance(err, errors.AIDEgenError):
exit_code = constant.EXIT_CODE_AIDEGEN_EXCEPTION
# Filter out sys.Exit(0) case, which is not an exception case.
if isinstance(err, SystemExit) and exc_value.code == 0:
exit_code = constant.EXIT_CODE_NORMAL
if exit_code is not constant.EXIT_CODE_NORMAL:
error_message = str(exc_value)
traceback_list = traceback.format_tb(exc_traceback)
traceback_list.append(error_message)
traceback_str = ''.join(traceback_list)
aidegen_metrics.ends_asuite_metrics(exit_code, traceback_str,
error_message)
# print out the trackback message for developers to debug
print(traceback_str)
raise err
finally:
if not ask_version:
print('\n{0} {1}\n'.format(_INFO, AIDEGEN_REPORT_LINK))
# Send the end message here on ignoring launch IDE case.
if not launch_ide and exit_code is constant.EXIT_CODE_NORMAL:
aidegen_metrics.ends_asuite_metrics(exit_code)
def aidegen_main(args):
"""AIDEGen main entry.
Try to generate project files for using in IDE. The process is:
1. Instantiate a ProjectConfig singleton object and initialize its
environment. After creating a singleton instance for ProjectConfig,
other modules can use project configurations by
ProjectConfig.get_instance().
2. Get an IDE instance from ide_util, ide_util.get_ide_util_instance will
use ProjectConfig.get_instance() inside the function.
3. Setup project_info.ProjectInfo.modules_info by instantiate
AidegenModuleInfo.
4. Check if projects contain C/C++ projects and launch related IDE.
Args:
args: A list of system arguments.
"""
config = project_config.ProjectConfig(args)
config.init_environment()
targets = config.targets
project_info.ProjectInfo.modules_info = module_info.AidegenModuleInfo()
cc_module_info = native_module_info.NativeModuleInfo()
jtargets, ctargets, rtargets = native_util.get_java_cc_and_rust_projects(
project_info.ProjectInfo.modules_info, cc_module_info, targets)
config.language, config.ide_name = common_util.determine_language_ide(
args.language[0], args.ide[0], jtargets, ctargets, rtargets)
# Called ide_util for pre-check the IDE existence state.
ide_util_obj = ide_util.get_ide_util_instance(
constant.IDE_DICT[config.ide_name])
all_langs = config.ide_name == constant.IDE_VSCODE
# Backward compatible strategy, when both java and C/C++ module exist,
# check the preferred target from the user and launch single one.
language_targets = {constant.JAVA: jtargets,
constant.C_CPP: ctargets,
constant.RUST: rtargets}
_launch_ide_by_module_contents(args, ide_util_obj, config.language,
language_targets, all_langs)
if __name__ == '__main__':
main(sys.argv[1:])