• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1#!/usr/bin/env python3
2#
3# Copyright 2018 - 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"""It is an AIDEGen sub task : generate the project files.
18
19This module generate IDE project files from templates.
20
21    Typical usage example:
22
23    generate_ide_project_file(project_info)
24"""
25
26import logging
27import os
28import pathlib
29import shutil
30
31from aidegen import constant
32from aidegen.lib import common_util
33
34# FACET_SECTION is a part of iml, which defines the framework of the project.
35_FACET_SECTION = '''\
36    <facet type="android" name="Android">
37        <configuration />
38    </facet>'''
39_SOURCE_FOLDER = ('            <sourceFolder url='
40                  '"file://%s" isTestSource="%s" />\n')
41_CONTENT_URL = '        <content url="file://%s">\n'
42_END_CONTENT = '        </content>\n'
43_ORDER_ENTRY = ('        <orderEntry type="module-library" exported="">'
44                '<library><CLASSES><root url="jar://%s!/" /></CLASSES>'
45                '<JAVADOC /><SOURCES /></library></orderEntry>\n')
46_MODULE_ORDER_ENTRY = ('        <orderEntry type="module" '
47                       'module-name="%s" />')
48_MODULE_SECTION = ('            <module fileurl="file:///$PROJECT_DIR$/%s.iml"'
49                   ' filepath="$PROJECT_DIR$/%s.iml" />')
50_SUB_MODULES_SECTION = ('            <module fileurl="file:///%s" '
51                        'filepath="%s" />')
52_VCS_SECTION = '        <mapping directory="%s" vcs="Git" />'
53_FACET_TOKEN = '@FACETS@'
54_SOURCE_TOKEN = '@SOURCES@'
55_MODULE_DEP_TOKEN = '@MODULE_DEPENDENCIES@'
56_MODULE_TOKEN = '@MODULES@'
57_VCS_TOKEN = '@VCS@'
58_JAVA_FILE_PATTERN = '%s/*.java'
59_ROOT_DIR = constant.AIDEGEN_ROOT_PATH
60_IDEA_DIR = os.path.join(_ROOT_DIR, 'templates/idea')
61_TEMPLATE_IML_PATH = os.path.join(_ROOT_DIR, 'templates/module-template.iml')
62_IDEA_FOLDER = '.idea'
63_MODULES_XML = 'modules.xml'
64_VCS_XML = 'vcs.xml'
65_TEMPLATE_MODULES_PATH = os.path.join(_IDEA_DIR, _MODULES_XML)
66_TEMPLATE_VCS_PATH = os.path.join(_IDEA_DIR, _VCS_XML)
67_DEPENDENCIES = 'dependencies'
68_DEPENDENCIES_IML = 'dependencies.iml'
69_COPYRIGHT_FOLDER = 'copyright'
70_CODE_STYLE_FOLDER = 'codeStyles'
71_COMPILE_XML = 'compiler.xml'
72_MISC_XML = 'misc.xml'
73_ANDROID_MANIFEST = 'AndroidManifest.xml'
74_IML_EXTENSION = '.iml'
75_FRAMEWORK_JAR = os.sep + 'framework.jar'
76_HIGH_PRIORITY_JARS = [_FRAMEWORK_JAR]
77_GIT_FOLDER_NAME = '.git'
78# Support gitignore by symbolic link to aidegen/data/gitignore_template.
79_GITIGNORE_FILE_NAME = '.gitignore'
80_GITIGNORE_REL_PATH = 'tools/asuite/aidegen/data/gitignore_template'
81_GITIGNORE_ABS_PATH = os.path.join(constant.ANDROID_ROOT_PATH,
82                                   _GITIGNORE_REL_PATH)
83# Support code style by symbolic link to aidegen/data/AndroidStyle_aidegen.xml.
84_CODE_STYLE_REL_PATH = 'tools/asuite/aidegen/data/AndroidStyle_aidegen.xml'
85_CODE_STYLE_SRC_PATH = os.path.join(constant.ANDROID_ROOT_PATH,
86                                    _CODE_STYLE_REL_PATH)
87
88_ECLIP_SRC_ENTRY = '<classpathentry exported="true" kind="src" path="{}"/>\n'
89_ECLIP_LIB_ENTRY = '<classpathentry exported="true" kind="lib" path="{}"/>\n'
90_ECLIP_TEMPLATE_PATH = os.path.join(_ROOT_DIR, 'templates/eclipse/eclipse.xml')
91_ECLIP_EXTENSION = '.classpath'
92_ECLIP_SRC_TOKEN = '@SRC@'
93_ECLIP_LIB_TOKEN = '@LIB@'
94_ECLIP_PROJECT_PATH = os.path.join(_ROOT_DIR, 'templates/eclipse/project.xml')
95_ECLIP_PROJECT_NAME_TOKEN = '@PROJECTNAME@'
96_ECLIP_PROJECT_EXTENSION = '.project'
97
98# b/121256503: Prevent duplicated iml names from breaking IDEA.
99# Use a map to cache in-using(already used) iml project file names.
100_USED_NAME_CACHE = dict()
101
102
103def get_unique_iml_name(abs_module_path):
104    """Create a unique iml name if needed.
105
106    If the name of last sub folder is used already, prefixing it with prior sub
107    folder names as a candidate name. If finally, it's unique, storing in
108    _USED_NAME_CACHE as: { abs_module_path:unique_name }. The cts case and UX of
109    IDE view are the main reasons why using module path strategy but not name of
110    module directly. Following is the detailed strategy:
111    1. While loop composes a sensible and shorter name, by checking unique to
112       finish the loop and finally add to cache.
113       Take ['cts', 'tests', 'app', 'ui'] an example, if 'ui' isn't occupied,
114       use it, else try 'cts_ui', then 'cts_app_ui', the worst case is whole
115       three candidate names are occupied already.
116    2. 'Else' for that while stands for no suitable name generated, so trying
117       'cts_tests_app_ui' directly. If it's still non unique, e.g., module path
118       cts/xxx/tests/app/ui occupied that name already, appending increasing
119       sequence number to get a unique name.
120
121    Args:
122        abs_module_path: Full module path string.
123
124    Return:
125        String: A unique iml name.
126    """
127    if abs_module_path in _USED_NAME_CACHE:
128        return _USED_NAME_CACHE[abs_module_path]
129
130    uniq_name = abs_module_path.strip(os.sep).split(os.sep)[-1]
131    if any(uniq_name == name for name in _USED_NAME_CACHE.values()):
132        parent_path = os.path.relpath(abs_module_path,
133                                      constant.ANDROID_ROOT_PATH)
134        sub_folders = parent_path.split(os.sep)
135        zero_base_index = len(sub_folders) - 1
136        # Start compose a sensible, shorter and unique name.
137        while zero_base_index > 0:
138            uniq_name = '_'.join(
139                [sub_folders[0], '_'.join(sub_folders[zero_base_index:])])
140            zero_base_index = zero_base_index - 1
141            if uniq_name not in _USED_NAME_CACHE.values():
142                break
143        else:
144            # b/133393638: To handle several corner cases.
145            uniq_name_base = parent_path.strip(os.sep).replace(os.sep, '_')
146            i = 0
147            uniq_name = uniq_name_base
148            while uniq_name in _USED_NAME_CACHE.values():
149                i = i + 1
150                uniq_name = '_'.join([uniq_name_base, str(i)])
151    _USED_NAME_CACHE[abs_module_path] = uniq_name
152    logging.debug('Unique name for module path of %s is %s.', abs_module_path,
153                  uniq_name)
154    return uniq_name
155
156
157def _generate_intellij_project_file(project_info, iml_path_list=None):
158    """Generates IntelliJ project file.
159
160    Args:
161        project_info: ProjectInfo instance.
162        iml_path_list: An optional list of submodule's iml paths, default None.
163    """
164    source_dict = dict.fromkeys(
165        list(project_info.source_path['source_folder_path']), False)
166    source_dict.update(
167        dict.fromkeys(list(project_info.source_path['test_folder_path']), True))
168    project_info.iml_path, _ = _generate_iml(
169        constant.ANDROID_ROOT_PATH, project_info.project_absolute_path,
170        source_dict, list(project_info.source_path['jar_path']),
171        project_info.project_relative_path)
172    _generate_modules_xml(project_info.project_absolute_path, iml_path_list)
173    project_info.git_path = _generate_vcs_xml(
174        project_info.project_absolute_path)
175    _copy_constant_project_files(project_info.project_absolute_path)
176
177
178def generate_ide_project_files(projects):
179    """Generate IDE project files by a list of ProjectInfo instances.
180
181    For multiple modules case, we call _generate_intellij_project_file to
182    generate iml file for submodules first and pass submodules' iml file paths
183    as an argument to function _generate_intellij_project_file when we generate
184    main module.iml file. In this way, we can add submodules' dependencies iml
185    and their own iml file paths to main module's module.xml.
186
187    Args:
188        projects: A list of ProjectInfo instances.
189    """
190    # Initialization
191    _USED_NAME_CACHE.clear()
192
193    for project in projects[1:]:
194        _generate_intellij_project_file(project)
195    iml_paths = [project.iml_path for project in projects[1:]]
196    _generate_intellij_project_file(projects[0], iml_paths)
197    _merge_project_vcs_xmls(projects)
198
199
200def _generate_eclipse_project_file(project_info):
201    """Generates Eclipse project file.
202
203    Args:
204        project_info: ProjectInfo instance.
205    """
206    module_path = project_info.project_absolute_path
207    module_name = get_unique_iml_name(module_path)
208    _generate_eclipse_project(module_name, module_path)
209    source_dict = dict.fromkeys(
210        list(project_info.source_path['source_folder_path']), False)
211    source_dict.update(
212        dict.fromkeys(list(project_info.source_path['test_folder_path']), True))
213    project_info.iml_path = _generate_classpath(
214        project_info.project_absolute_path, list(sorted(source_dict)),
215        list(project_info.source_path['jar_path']))
216
217
218def generate_eclipse_project_files(projects):
219    """Generate Eclipse project files by a list of ProjectInfo instances.
220
221    Args:
222        projects: A list of ProjectInfo instances.
223    """
224    for project in projects:
225        _generate_eclipse_project_file(project)
226
227
228def _read_file_content(path):
229    """Read file's content.
230
231    Args:
232        path: Path of input file.
233
234    Returns:
235        String: Content of the file.
236    """
237    with open(path) as template:
238        return template.read()
239
240
241def _file_generate(path, content):
242    """Generate file from content.
243
244    Args:
245        path: Path of target file.
246        content: String content of file.
247    """
248    if not os.path.exists(os.path.dirname(path)):
249        os.makedirs(os.path.dirname(path))
250    with open(path, 'w') as target:
251        target.write(content)
252
253
254def _copy_constant_project_files(target_path):
255    """Copy project files to target path with error handling.
256
257    This function would copy compiler.xml, misc.xml, codeStyles folder and
258    copyright folder to target folder. Since these files aren't mandatory in
259    IntelliJ, it only logs when an IOError occurred.
260
261    Args:
262        target_path: A folder path to copy content to.
263    """
264    try:
265        _copy_to_idea_folder(target_path, _COPYRIGHT_FOLDER)
266        _copy_to_idea_folder(target_path, _CODE_STYLE_FOLDER)
267        code_style_target_path = os.path.join(target_path, _IDEA_FOLDER,
268                                              _CODE_STYLE_FOLDER, 'Project.xml')
269        # Base on current working directory to prepare the relevant location
270        # of the symbolic link file, and base on the symlink file location to
271        # prepare the relevant code style source path.
272        rel_target = os.path.relpath(code_style_target_path, os.getcwd())
273        rel_source = os.path.relpath(_CODE_STYLE_SRC_PATH,
274                                     os.path.dirname(code_style_target_path))
275        logging.debug('Relative target symlink path: %s.', rel_target)
276        logging.debug('Relative code style source path: %s.', rel_source)
277        os.symlink(rel_source, rel_target)
278        # Create .gitignore if it doesn't exist.
279        _generate_git_ignore(target_path)
280        shutil.copy(
281            os.path.join(_IDEA_DIR, _COMPILE_XML),
282            os.path.join(target_path, _IDEA_FOLDER, _COMPILE_XML))
283        shutil.copy(
284            os.path.join(_IDEA_DIR, _MISC_XML),
285            os.path.join(target_path, _IDEA_FOLDER, _MISC_XML))
286    except IOError as err:
287        logging.warning('%s can\'t copy the project files\n %s', target_path,
288                        err)
289
290
291def _copy_to_idea_folder(target_path, folder_name):
292    """Copy folder to project .idea path.
293
294    Args:
295        target_path: Path of target folder.
296        folder_name: Name of target folder.
297    """
298    target_folder_path = os.path.join(target_path, _IDEA_FOLDER, folder_name)
299    # Existing folder needs to be removed first, otherwise it will raise
300    # IOError.
301    if os.path.exists(target_folder_path):
302        shutil.rmtree(target_folder_path)
303    shutil.copytree(os.path.join(_IDEA_DIR, folder_name), target_folder_path)
304
305
306def _handle_facet(content, path):
307    """Handle facet part of iml.
308
309    If the module is an Android app, which contains AndroidManifest.xml, it
310    should have a facet of android, otherwise we don't need facet in iml.
311
312    Args:
313        content: String content of iml.
314        path: Path of the module.
315
316    Returns:
317        String: Content with facet handled.
318    """
319    facet = ''
320    if os.path.isfile(os.path.join(path, _ANDROID_MANIFEST)):
321        facet = _FACET_SECTION
322    return content.replace(_FACET_TOKEN, facet)
323
324
325def _handle_module_dependency(root_path, content, jar_dependencies):
326    """Handle module dependency part of iml.
327
328    Args:
329        root_path: Android source tree root path.
330        content: String content of iml.
331        jar_dependencies: List of the jar path.
332
333    Returns:
334        String: Content with module dependency handled.
335    """
336    module_library = ''
337    dependencies = []
338    # Reorder deps in the iml generated by IntelliJ by inserting priority jars.
339    for jar_path in jar_dependencies:
340        if any((jar_path.endswith(high_priority_jar))
341               for high_priority_jar in _HIGH_PRIORITY_JARS):
342            module_library += _ORDER_ENTRY % os.path.join(root_path, jar_path)
343        else:
344            dependencies.append(jar_path)
345
346    # IntelliJ indexes jars as dependencies from iml by the ascending order.
347    # Without sorting, the order of jar list changes everytime. Sort the jar
348    # list to keep the jar dependencies in consistency. It also can help us to
349    # discover potential issues like duplicated classes.
350    for jar_path in sorted(dependencies):
351        module_library += _ORDER_ENTRY % os.path.join(root_path, jar_path)
352    return content.replace(_MODULE_DEP_TOKEN, module_library)
353
354
355def _is_project_relative_source(source, relative_path):
356    """Check if the relative path of a file is a source relative path.
357
358    Check if the file path starts with the relative path or the relative is an
359    Android source tree root path.
360
361    Args:
362        source: The file path to be checked.
363        relative_path: The relative path to be checked.
364
365    Returns:
366        True if the file is a source relative path, otherwise False.
367    """
368    abs_path = common_util.get_abs_path(relative_path)
369    if common_util.is_android_root(abs_path):
370        return True
371    if _is_source_under_relative_path(source, relative_path):
372        return True
373    return False
374
375
376def _handle_source_folder(root_path, content, source_dict, is_module,
377                          relative_path):
378    """Handle source folder part of iml.
379
380    It would make the source folder group by content.
381    e.g.
382    <content url="file://$MODULE_DIR$/a">
383        <sourceFolder url="file://$MODULE_DIR$/a/b" isTestSource="False" />
384        <sourceFolder url="file://$MODULE_DIR$/a/test" isTestSource="True" />
385        <sourceFolder url="file://$MODULE_DIR$/a/d/e" isTestSource="False" />
386    </content>
387
388    Args:
389        root_path: Android source tree root path.
390        content: String content of iml.
391        source_dict: A dictionary of sources path with a flag to identify the
392                     path is test or source folder in IntelliJ.
393                     e.g.
394                     {'path_a': True, 'path_b': False}
395        is_module: True if it is module iml, otherwise it is dependencies iml.
396        relative_path: Relative path of the module.
397
398    Returns:
399        String: Content with source folder handled.
400    """
401    source_list = list(source_dict.keys())
402    source_list.sort()
403    src_builder = []
404    if is_module:
405        # Set the content url to module's path since it's the iml of target
406        # project which only has it's sub-folders in source_list.
407        src_builder.append(
408            _CONTENT_URL % os.path.join(root_path, relative_path))
409        for path, is_test_flag in sorted(source_dict.items()):
410            if _is_project_relative_source(path, relative_path):
411                src_builder.append(_SOURCE_FOLDER % (os.path.join(
412                    root_path, path), is_test_flag))
413        src_builder.append(_END_CONTENT)
414    else:
415        for path, is_test_flag in sorted(source_dict.items()):
416            path = os.path.join(root_path, path)
417            src_builder.append(_CONTENT_URL % path)
418            src_builder.append(_SOURCE_FOLDER % (path, is_test_flag))
419            src_builder.append(_END_CONTENT)
420    return content.replace(_SOURCE_TOKEN, ''.join(src_builder))
421
422
423def _trim_same_root_source(source_list):
424    """Trim the source which has the same root.
425
426    The source list may contain lots of duplicate sources.
427    For example:
428    a/b, a/b/c, a/b/d
429    We only need to import a/b in iml, this function is used to trim redundant
430    sources.
431
432    Args:
433        source_list: Sorted list of the sources.
434
435    Returns:
436        List: The trimmed source list.
437    """
438    tmp_source_list = [source_list[0]]
439    for src_path in source_list:
440        if ''.join([tmp_source_list[-1],
441                    os.sep]) not in ''.join([src_path, os.sep]):
442            tmp_source_list.append(src_path)
443    return sorted(tmp_source_list)
444
445
446def _is_source_under_relative_path(source, relative_path):
447    """Check if a source file is a project relative path file.
448
449    Args:
450        source: Android source file path.
451        relative_path: Relative path of the module.
452
453    Returns:
454        True if source file is a project relative path file, otherwise False.
455    """
456    return source == relative_path or source.startswith(relative_path + os.sep)
457
458
459# pylint: disable=too-many-locals
460def _generate_iml(root_path, module_path, source_dict, jar_dependencies,
461                  relative_path):
462    """Generate iml file.
463
464    Args:
465        root_path: Android source tree root path.
466        module_path: Absolute path of the module.
467        source_dict: A dictionary of sources path with a flag to distinguish the
468                     path is test or source folder in IntelliJ.
469                     e.g.
470                     {'path_a': True, 'path_b': False}
471        jar_dependencies: List of the jar path.
472        relative_path: Relative path of the module.
473
474    Returns:
475        String: The absolute paths of module iml and dependencies iml.
476    """
477    template = _read_file_content(_TEMPLATE_IML_PATH)
478
479    # Separate module and dependencies source folder
480    project_source_dict = {}
481    for source in list(source_dict):
482        if _is_project_relative_source(source, relative_path):
483            is_test = source_dict.get(source)
484            source_dict.pop(source)
485            project_source_dict.update({source: is_test})
486
487    # Generate module iml.
488    module_content = _handle_facet(template, module_path)
489    module_content = _handle_source_folder(
490        root_path, module_content, project_source_dict, True, relative_path)
491    # b/121256503: Prevent duplicated iml names from breaking IDEA.
492    module_name = get_unique_iml_name(module_path)
493
494    module_iml_path = os.path.join(module_path, module_name + _IML_EXTENSION)
495
496    dep_name = _get_dependencies_name(module_name)
497    dep_sect = _MODULE_ORDER_ENTRY % dep_name
498    module_content = module_content.replace(_MODULE_DEP_TOKEN, dep_sect)
499    _file_generate(module_iml_path, module_content)
500
501    # Generate dependencies iml.
502    dependencies_content = template.replace(_FACET_TOKEN, '')
503    dependencies_content = _handle_source_folder(
504        root_path, dependencies_content, source_dict, False, relative_path)
505    dependencies_content = _handle_module_dependency(
506        root_path, dependencies_content, jar_dependencies)
507    dependencies_iml_path = os.path.join(module_path, dep_name + _IML_EXTENSION)
508    _file_generate(dependencies_iml_path, dependencies_content)
509    logging.debug('Paired iml names are %s, %s', module_iml_path,
510                  dependencies_iml_path)
511    # The dependencies_iml_path is use for removing the file itself in unittest.
512    return module_iml_path, dependencies_iml_path
513
514
515def _generate_classpath(module_path, source_list, jar_dependencies):
516    """Generate .classpath file.
517
518    Args:
519        module_path: Absolute path of the module.
520        source_list: A list of sources path.
521        jar_dependencies: List of the jar path.
522
523    Returns:
524        String: The absolute paths of .classpath.
525    """
526    template = _read_file_content(_ECLIP_TEMPLATE_PATH)
527
528    src_list = [_ECLIP_SRC_ENTRY.format(s) for s in source_list]
529    template = template.replace(_ECLIP_SRC_TOKEN, ''.join(src_list))
530
531    lib_list = [_ECLIP_LIB_ENTRY.format(j) for j in jar_dependencies]
532    template = template.replace(_ECLIP_LIB_TOKEN, ''.join(lib_list))
533
534    classpath_path = os.path.join(module_path, _ECLIP_EXTENSION)
535
536    _file_generate(classpath_path, template)
537
538    return classpath_path
539
540
541def _generate_eclipse_project(project_name, module_path):
542    """Generate .project file of Eclipse.
543
544    Args:
545        project_name: A string of the project name.
546        module_path: Absolute path of the module.
547    """
548    template = _read_file_content(_ECLIP_PROJECT_PATH)
549    template = template.replace(_ECLIP_PROJECT_NAME_TOKEN, project_name)
550    eclipse_project = os.path.join(module_path, _ECLIP_PROJECT_EXTENSION)
551    _file_generate(eclipse_project, template)
552
553
554def _get_dependencies_name(module_name):
555    """Get module's dependencies iml name which will be written in module.xml.
556
557    Args:
558        module_name: The name will be appended to "dependencies-".
559
560    Returns:
561        String: The joined dependencies iml file name, e.g. "dependencies-core"
562    """
563    return '-'.join([_DEPENDENCIES, module_name])
564
565
566def _generate_modules_xml(module_path, iml_path_list=None):
567    """Generate modules.xml file.
568
569    IntelliJ uses modules.xml to import which modules should be loaded to
570    project. Only in multiple modules case will we pass iml_path_list of
571    submodules' dependencies and their iml file paths to add them into main
572    module's module.xml file. The dependencies iml file names will be changed
573    from original dependencies.iml to dependencies-[module_name].iml,
574    e.g. dependencies-core.iml for core.iml.
575
576    Args:
577        module_path: Path of the module.
578        iml_path_list: A list of submodule iml paths.
579    """
580    content = _read_file_content(_TEMPLATE_MODULES_PATH)
581
582    # b/121256503: Prevent duplicated iml names from breaking IDEA.
583    module_name = get_unique_iml_name(module_path)
584
585    file_name = os.path.splitext(module_name)[0]
586    dep_name = _get_dependencies_name(file_name)
587    module_list = [
588        _MODULE_SECTION % (module_name, module_name),
589        _MODULE_SECTION % (dep_name, dep_name)
590    ]
591    if iml_path_list:
592        for iml_path in iml_path_list:
593            iml_dir, iml_name = os.path.split(iml_path)
594            dep_file = _get_dependencies_name(iml_name)
595            dep_path = os.path.join(iml_dir, dep_file)
596            module_list.append(_SUB_MODULES_SECTION % (dep_path, dep_path))
597            module_list.append(_SUB_MODULES_SECTION % (iml_path, iml_path))
598    module = '\n'.join(module_list)
599    content = content.replace(_MODULE_TOKEN, module)
600    target_path = os.path.join(module_path, _IDEA_FOLDER, _MODULES_XML)
601    _file_generate(target_path, content)
602
603
604def _generate_vcs_xml(module_path):
605    """Generate vcs.xml file.
606
607    IntelliJ use vcs.xml to record version control software's information.
608    Since we are using a single project file, it will only contain the
609    module itself. If there is no git folder inside, it would find it in
610    parent's folder.
611
612    Args:
613        module_path: Path of the module.
614
615    Return:
616        String: A module's git path.
617    """
618    git_path = module_path
619    while not os.path.isdir(os.path.join(git_path, _GIT_FOLDER_NAME)):
620        git_path = str(pathlib.Path(git_path).parent)
621        if git_path == os.sep:
622            logging.warning('%s can\'t find its .git folder', module_path)
623            return None
624    _write_vcs_xml(module_path, [git_path])
625    return git_path
626
627
628def _write_vcs_xml(module_path, git_paths):
629    """Write the git path into vcs.xml.
630
631    For main module, the vcs.xml should include all modules' git path.
632    For submodules, there is only one git path in vcs.xml.
633
634    Args:
635        module_path: Path of the module.
636        git_paths: A list of git path.
637    """
638    _vcs_content = '\n'.join([_VCS_SECTION % p for p in git_paths if p])
639    content = _read_file_content(_TEMPLATE_VCS_PATH)
640    content = content.replace(_VCS_TOKEN, _vcs_content)
641    target_path = os.path.join(module_path, _IDEA_FOLDER, _VCS_XML)
642    _file_generate(target_path, content)
643
644
645def _merge_project_vcs_xmls(projects):
646    """Merge sub projects' git paths into main project's vcs.xml.
647
648    After all projects' vcs.xml are generated, collect the git path of each
649    projects and write them into main project's vcs.xml.
650
651    Args:
652        projects: A list of ProjectInfo instances.
653    """
654    main_project_absolute_path = projects[0].project_absolute_path
655    git_paths = [project.git_path for project in projects]
656    _write_vcs_xml(main_project_absolute_path, git_paths)
657
658
659def _generate_git_ignore(target_folder):
660    """Generate .gitignore file.
661
662    In target_folder, if there's no .gitignore file, uses symlink() to generate
663    one to hide project content files from git.
664
665    Args:
666        target_folder: An absolute path string of target folder.
667    """
668    # TODO(b/133639849): Provide a common method to create symbolic link.
669    # TODO(b/133641803): Move out aidegen artifacts from Android repo.
670    try:
671        gitignore_abs_path = os.path.join(target_folder, _GITIGNORE_FILE_NAME)
672        rel_target = os.path.relpath(gitignore_abs_path, os.getcwd())
673        rel_source = os.path.relpath(_GITIGNORE_ABS_PATH, target_folder)
674        logging.debug('Relative target symlink path: %s.', rel_target)
675        logging.debug('Relative ignore_template source path: %s.', rel_source)
676        if not os.path.exists(gitignore_abs_path):
677            os.symlink(rel_source, rel_target)
678    except OSError as err:
679        logging.error('Not support to run aidegen on Windows.\n %s', err)
680