1#!/usr/bin/env python3
2#
3# Copyright 2019 - The Android Open Source Project
4#
5# Licensed under the Apache License, Version 2.0 (the "License");
6# you may not use this file except in compliance with the License.
7# You may obtain a copy of the License at
8#
9#     http://www.apache.org/licenses/LICENSE-2.0
10#
11# Unless required by applicable law or agreed to in writing, software
12# distributed under the License is distributed on an "AS IS" BASIS,
13# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14# See the License for the specific language governing permissions and
15# limitations under the License.
16
17"""ide_common_util
18
19This module has a collection of helper functions of the ide_util module.
20"""
21
22import fnmatch
23import glob
24import logging
25import os
26import subprocess
27
28from aidegen import constant
29
30_IDEA_FOLDER = '.idea'
31_IML_EXTENSION = '.iml'
32
33
34def get_script_from_internal_path(ide_paths, ide_name):
35    """Get the studio.sh script path from internal path.
36
37    Args:
38        ide_paths: A list of IDE installed paths to be checked.
39        ide_name: The IDE name.
40
41    Returns:
42        The list of the full path of IDE or None if the IDE doesn't exist.
43    """
44    for ide_path in ide_paths:
45        ls_output = glob.glob(ide_path, recursive=True)
46        ls_output = sorted(ls_output)
47        if ls_output:
48            logging.debug('The script%s for %s %s found.',
49                          's' if len(ls_output) > 1 else '', ide_name,
50                          'are' if len(ls_output) > 1 else 'is')
51            return ls_output
52    logging.error('There is not any script of %s found.', ide_name)
53    return None
54
55
56def _run_ide_sh(run_sh_cmd, project_path):
57    """Run IDE launching script with an IntelliJ project path as argument.
58
59    Args:
60        run_sh_cmd: The command to launch IDE.
61        project_path: The path of IntelliJ IDEA project content.
62    """
63    assert run_sh_cmd, 'No suitable IDE installed.'
64    logging.debug('Run command: "%s" to launch project.', run_sh_cmd)
65    try:
66        subprocess.check_call(run_sh_cmd, shell=True)
67    except subprocess.CalledProcessError as err:
68        logging.error('Launch project path %s failed with error: %s.',
69                      project_path, err)
70
71
72def _walk_tree_find_ide_exe_file(top, ide_script_name):
73    """Recursively descend the directory tree rooted at top and filter out the
74       IDE executable script we need.
75
76    Args:
77        top: the tree root to be checked.
78        ide_script_name: IDE file name such i.e. IdeIntelliJ._INTELLIJ_EXE_FILE.
79
80    Returns:
81        the IDE executable script file(s) found.
82    """
83    logging.info('Searching IDE script %s in path: %s.', ide_script_name, top)
84    for root, _, files in os.walk(top):
85        logging.debug('Search all files under %s to get %s, %s.', top, root,
86                      files)
87        for file_ in fnmatch.filter(files, ide_script_name):
88            exe_file = os.path.join(root, file_)
89            if os.access(exe_file, os.X_OK):
90                logging.debug('Use file name filter to find %s in path %s.',
91                              file_, exe_file)
92                yield exe_file
93
94
95def get_run_ide_cmd(sh_path, project_file, new_process=True):
96    """Get the command to launch IDE.
97
98    Args:
99        sh_path: The idea.sh path where IDE is installed.
100        project_file: The path of IntelliJ IDEA project file.
101        new_process: Default is True, means to run command in a new process.
102
103    Returns:
104        A string: The IDE launching command.
105    """
106    process_flag = '&' if new_process else ''
107    # In command usage, the space ' ' should be '\ ' for correctness.
108    return ' '.join([
109        constant.NOHUP, sh_path.replace(' ', r'\ '), project_file,
110        constant.IGNORE_STD_OUT_ERR_CMD, process_flag
111    ])
112
113
114def _get_scripts_from_file_path(input_path, ide_file_name):
115    """Get IDE executable script file from input file path.
116
117    Args:
118        input_path: the file path to be checked.
119        ide_file_name: the IDE executable script file name.
120
121    Returns:
122        A list of the IDE executable script path if exists otherwise None.
123    """
124    if os.path.basename(input_path).startswith(ide_file_name):
125        files_found = glob.glob(input_path)
126        if files_found:
127            return sorted(files_found)
128    return None
129
130
131def get_scripts_from_dir_path(input_path, ide_file_name):
132    """Get an IDE executable script file from input directory path.
133
134    Args:
135        input_path: the directory to be searched.
136        ide_file_name: the IDE executable script file name.
137
138    Returns:
139        A list of an IDE executable script paths if exist otherwise None.
140    """
141    logging.debug('Call get_scripts_from_dir_path with %s, and %s', input_path,
142                  ide_file_name)
143    files_found = list(_walk_tree_find_ide_exe_file(input_path,
144                                                    ide_file_name + '*'))
145    if files_found:
146        return sorted(files_found)
147    return None
148
149
150def launch_ide(project_path, run_ide_cmd, ide_name):
151    """Launches relative IDE by opening the passed project file.
152
153    Args:
154        project_path: The full path of the IDE project content.
155        run_ide_cmd: The command to launch IDE.
156        ide_name: the IDE name is to be launched.
157    """
158    assert project_path, 'Empty content path is not allowed.'
159    if ide_name == constant.IDE_ECLIPSE:
160        logging.info(
161            'Launch %s with workspace: %s.', ide_name, constant.ECLIPSE_WS)
162    else:
163        logging.info('Launch %s for project content path: %s.', ide_name,
164                     project_path)
165    _run_ide_sh(run_ide_cmd, project_path)
166
167
168def is_intellij_project(project_path):
169    """Checks if the path passed in is an IntelliJ project content.
170
171    Args:
172        project_path: The full path of IDEA project content, which contains
173        .idea folder and .iml file(s).
174
175    Returns:
176        True if project_path is an IntelliJ project, False otherwise.
177    """
178    if not os.path.isfile(project_path):
179        return os.path.isdir(project_path) and os.path.isdir(
180            os.path.join(project_path, _IDEA_FOLDER))
181
182    _, ext = os.path.splitext(os.path.basename(project_path))
183    if ext and _IML_EXTENSION == ext.lower():
184        path = os.path.dirname(project_path)
185        logging.debug('Extracted path is: %s.', path)
186        return os.path.isdir(os.path.join(path, _IDEA_FOLDER))
187    return False
188
189
190def get_script_from_input_path(input_path, ide_file_name):
191    """Get correct IntelliJ executable script path from input path.
192
193    1. If input_path is a file, check if it is an IDE executable script file.
194    2. It input_path is a directory, search if it contains IDE executable script
195       file(s).
196
197    Args:
198        input_path: input path to be checked if it's an IDE executable
199                    script.
200        ide_file_name: the IDE executable script file name.
201
202    Returns:
203        IDE executable file(s) if exists otherwise None.
204    """
205    if not input_path:
206        return None
207    ide_path = []
208    if os.path.isfile(input_path):
209        ide_path = _get_scripts_from_file_path(input_path, ide_file_name)
210    if os.path.isdir(input_path):
211        ide_path = get_scripts_from_dir_path(input_path, ide_file_name)
212    if ide_path:
213        logging.debug('IDE installed path from user input: %s.', ide_path)
214        return ide_path
215    return None
216
217
218def get_intellij_version_path(version_path):
219    """Locates the IntelliJ IDEA launch script path by version.
220
221    Args:
222        version_path: IntelliJ CE or UE version launch script path.
223
224    Returns:
225        A list of the sh full paths, or None if no such IntelliJ version is
226        installed.
227    """
228    ls_output = glob.glob(version_path, recursive=True)
229    if not ls_output:
230        return None
231    ls_output = sorted(ls_output, reverse=True)
232    logging.debug('Result for checking IntelliJ path %s after sorting:%s.',
233                  version_path, ls_output)
234    return ls_output
235
236
237def ask_preference(all_versions, ide_name):
238    """Ask users which version they prefer.
239
240    Args:
241        all_versions: A list of all CE and UE version launch script paths.
242        ide_name: The IDE name is going to be launched.
243
244    Returns:
245        An users selected version.
246    """
247    options = []
248    for i, sfile in enumerate(all_versions, 1):
249        options.append('\t{}. {}'.format(i, sfile))
250    query = ('You installed {} versions of {}:\n{}\nPlease select '
251             'one.\t').format(len(all_versions), ide_name, '\n'.join(options))
252    return _select_intellij_version(query, all_versions)
253
254
255def _select_intellij_version(query, all_versions):
256    """Select one from different IntelliJ versions users installed.
257
258    Args:
259        query: The query message.
260        all_versions: A list of all CE and UE version launch script paths.
261    """
262    all_numbers = []
263    for i in range(len(all_versions)):
264        all_numbers.append(str(i + 1))
265    input_data = input(query)
266    while input_data not in all_numbers:
267        input_data = input('Please select a number:\t')
268    return all_versions[int(input_data) - 1]
269