1#!/usr/bin/env python3
2#
3# Copyright (C) 2020 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#
17import sys
18import os
19import argparse
20from datetime import date
21import subprocess
22from enum import Enum
23from textwrap import dedent
24from shutil import rmtree
25from shutil import copyfile
26from shutil import copytree
27import re
28
29try:
30    # non-default python3 module, be helpful if it is missing
31    import toml
32except ModuleNotFoundError as e:
33    print(e)
34    print("Consider running `pip install toml` to install this module")
35    exit(-1)
36
37# cd into directory of script
38os.chdir(os.path.dirname(os.path.abspath(__file__)))
39
40FRAMEWORKS_SUPPORT_FP = os.path.abspath(os.path.join(os.getcwd(), '..', '..'))
41SAMPLE_OWNERS_FP = os.path.abspath(os.path.join(os.getcwd(), 'kotlin-template', 'groupId', 'OWNERS'))
42SAMPLE_JAVA_SRC_FP = os.path.abspath(os.path.join(os.getcwd(), 'java-template', 'groupId', 'artifactId'))
43SAMPLE_KOTLIN_SRC_FP = os.path.abspath(os.path.join(os.getcwd(), 'kotlin-template', 'groupId', 'artifactId'))
44SAMPLE_COMPOSE_SRC_FP = os.path.abspath(os.path.join(os.getcwd(), 'compose-template', 'groupId', 'artifactId'))
45NATIVE_SRC_FP = os.path.abspath(os.path.join(os.getcwd(), 'native-template', 'groupId', 'artifactId'))
46SETTINGS_GRADLE_FP = os.path.abspath(os.path.join(os.getcwd(), '..', '..', "settings.gradle"))
47LIBRARY_VERSIONS_REL = './libraryversions.toml'
48LIBRARY_VERSIONS_FP = os.path.join(FRAMEWORKS_SUPPORT_FP, LIBRARY_VERSIONS_REL)
49DOCS_TOT_BUILD_GRADLE_REL = './docs-tip-of-tree/build.gradle'
50DOCS_TOT_BUILD_GRADLE_FP = os.path.join(FRAMEWORKS_SUPPORT_FP, DOCS_TOT_BUILD_GRADLE_REL)
51
52# Set up input arguments
53parser = argparse.ArgumentParser(
54    description=("""Genereates new project in androidx."""))
55parser.add_argument(
56    'group_id',
57    help='group_id for the new library')
58parser.add_argument(
59    'artifact_id',
60    help='artifact_id for the new library')
61
62
63class ProjectType(Enum):
64    KOTLIN = 0
65    JAVA = 1
66    NATIVE = 2
67
68def print_e(*args, **kwargs):
69    print(*args, file=sys.stderr, **kwargs)
70
71def cp(src_path_dir, dst_path_dir):
72    """Copies all files in the src_path_dir into the dst_path_dir
73
74    Args:
75        src_path_dir: the source directory, which must exist
76        dst_path_dir: the distination directory
77    """
78    if not os.path.exists(dst_path_dir):
79        os.makedirs(dst_path_dir)
80    if not os.path.exists(src_path_dir):
81        print_e('cp error: Source path %s does not exist.' % src_path_dir)
82        return None
83    try:
84        copytree(src_path_dir, dst_path_dir, dirs_exist_ok=True)
85    except Error as err:
86        print_e('FAIL: Unable to copy %s to destination %s' % (src_path_dir, dst_path_dir))
87        return None
88    return dst_path_dir
89
90def rm(path):
91    if os.path.isdir(path):
92        rmtree(path)
93    elif os.path.exists(path):
94        os.remove(path)
95
96def mv_dir(src_path_dir, dst_path_dir):
97    """Moves a directory from src_path_dir to dst_path_dir.
98
99    Args:
100        src_path_dir: the source directory, which must exist
101        dst_path_dir: the distination directory
102    """
103    if os.path.exists(dst_path_dir):
104        print_e('rename error: Destination path %s already exists.' % dst_path_dir)
105        return None
106    # If moving to a new parent directory, create that directory
107    parent_dst_path_dir = os.path.dirname(dst_path_dir)
108    if not os.path.exists(parent_dst_path_dir):
109        os.makedirs(parent_dst_path_dir)
110    if not os.path.exists(src_path_dir):
111        print_e('mv error: Source path %s does not exist.' % src_path_dir)
112        return None
113    try:
114        os.rename(src_path_dir, dst_path_dir)
115    except OSError as error:
116        print_e('FAIL: Unable to copy %s to destination %s' % (src_path_dir, dst_path_dir))
117        print_e(error)
118        return None
119    return dst_path_dir
120
121def rename_file(src_file, new_file_name):
122    """Renames a file from src_file to new_file_name, within the same directory.
123
124    Args:
125        src_file: the source file, which must exist
126        new_file_name: the new file name
127    """
128    if not os.path.exists(src_file):
129        print_e('mv file error: Source file %s does not exist.' % src_file)
130        return None
131    # Check that destination directory already exists
132    parent_src_file_dir = os.path.dirname(src_file)
133    new_file_path = os.path.join(parent_src_file_dir, new_file_name)
134    if os.path.exists(new_file_path):
135        print_e('mv file error: Source file %s already exists.' % new_file_path)
136        return None
137    try:
138        os.rename(src_file, new_file_path)
139    except OSError as error:
140        print_e('FAIL: Unable to rename %s to destination %s' % (src_file, new_file_path))
141        print_e(error)
142        return None
143    return new_file_path
144
145def create_file(path):
146    """
147    Creates an empty file if it does not already exist.
148    """
149    open(path, "a").close()
150
151def generate_package_name(group_id, artifact_id):
152    final_group_id_word = group_id.split(".")[-1]
153    artifact_id_suffix = re.sub(r"\b%s\b" % final_group_id_word, "", artifact_id)
154    artifact_id_suffix = artifact_id_suffix.replace("-", ".")
155    if (final_group_id_word == artifact_id):
156      return group_id +  artifact_id_suffix
157    elif (final_group_id_word != artifact_id):
158      if ("." in artifact_id_suffix):
159        return group_id +  artifact_id_suffix
160      else:
161        return group_id + "." + artifact_id_suffix
162
163def validate_name(group_id, artifact_id):
164    if not group_id.startswith("androidx."):
165        print_e("Group ID must start with androidx.")
166        return False
167    final_group_id_word = group_id.split(".")[-1]
168    if not artifact_id.startswith(final_group_id_word):
169        print_e("Artifact ID must use the final word in the group Id " + \
170                "as the prefix.  For example, `androidx.foo.bar:bar-qux`" + \
171                "or `androidx.foo:foo-bar` are valid names.")
172        return False
173    return True
174
175def get_year():
176    return str(date.today().year)
177
178def get_group_id_version_macro(group_id):
179    group_id_version_macro = group_id.replace("androidx.", "").replace(".", "_").upper()
180    if group_id == "androidx.compose":
181        group_id_version_macro = "COMPOSE"
182    elif group_id.startswith("androidx.compose"):
183        group_id_version_macro = group_id.replace("androidx.compose.", "").replace(".",
184                                                  "_").upper()
185    return group_id_version_macro
186
187def sed(before, after, file):
188    with open(file) as f:
189       file_contents = f.read()
190    new_file_contents = file_contents.replace(before, after)
191    # write back the file
192    with open(file,"w") as f:
193        f.write(new_file_contents)
194
195def remove_line(line_to_remove, file):
196    with open(file) as f:
197       file_contents = f.readlines()
198    new_file_contents = []
199    for line in file_contents:
200        if line_to_remove not in line:
201            new_file_contents.append(line)
202    # write back the file
203    with open(file,"w") as f:
204        f.write("".join(new_file_contents))
205
206def ask_yes_or_no(question):
207    while(True):
208        reply = str(input(question+' (y/n): ')).lower().strip()
209        if reply:
210            if reply[0] == 'y': return True
211            if reply[0] == 'n': return False
212        print("Please respond with y/n")
213
214def ask_project_type():
215    """Asks the user which type of project they wish to create"""
216    message = dedent("""
217        Please choose the type of project you would like to create:
218        1: Kotlin (AAR)
219        2: Java (AAR / JAR)
220        3: Native (AAR)
221    """).strip()
222    while(True):
223        reply = str(input(message + "\n")).strip()
224        if reply == "1": return ProjectType.KOTLIN
225        if reply == "2":
226            if confirm_java_project_type():
227                return ProjectType.JAVA
228        if reply == "3": return ProjectType.NATIVE
229        print("Please respond with one of the presented options")
230
231def confirm_java_project_type():
232    return ask_yes_or_no("All new androidx projects are expected and encouraged "
233    "to use Kotlin. Java projects should only be used if "
234    "there is a business need to do so. "
235    "Please ack to proceed:")
236
237def ask_library_purpose():
238    question = ("Project description (please complete the sentence): "
239        "This library makes it easy for developers to... ")
240    while(True):
241        reply = str(input(question)).strip()
242        if reply: return reply
243        print("Please input a description!")
244
245def ask_project_description():
246    question = ("Please provide a project description: ")
247    while(True):
248        reply = str(input(question)).strip()
249        if reply: return reply
250        print("Please input a description!")
251
252def get_gradle_project_coordinates(group_id, artifact_id):
253    coordinates = group_id.replace("androidx", "").replace(".",":")
254    coordinates += ":" + artifact_id
255    return coordinates
256
257def run_update_api(group_id, artifact_id):
258    gradle_coordinates = get_gradle_project_coordinates(group_id, artifact_id)
259    gradle_cmd = "cd " + FRAMEWORKS_SUPPORT_FP + " && ./gradlew " + gradle_coordinates + ":updateApi"
260    try:
261        subprocess.check_output(gradle_cmd, stderr=subprocess.STDOUT, shell=True)
262    except subprocess.CalledProcessError:
263        print_e('FAIL: Unable run updateApi with command: %s' % gradle_cmd)
264        return None
265    return True
266
267def get_library_type(artifact_id):
268    """Returns the appropriate androidx.build.SoftwareType for the project.
269    """
270    if "sample" in artifact_id:
271        library_type = "SAMPLES"
272    elif "compiler" in artifact_id:
273        library_type = "ANNOTATION_PROCESSOR"
274    elif "lint" in artifact_id:
275        library_type = "LINT"
276    elif "inspection" in artifact_id:
277        library_type = "IDE_PLUGIN"
278    else:
279        library_type = "PUBLISHED_LIBRARY"
280    return library_type
281
282def get_group_id_path(group_id):
283    """Generates the group ID filepath
284
285    Given androidx.foo.bar, the structure will be:
286    frameworks/support/foo/bar
287
288    Args:
289        group_id: group_id of the new library
290    """
291    return FRAMEWORKS_SUPPORT_FP + "/" + group_id.replace("androidx.", "").replace(".", "/")
292
293def get_full_artifact_path(group_id, artifact_id):
294    """Generates the full artifact ID filepath
295
296    Given androidx.foo.bar:bar-qux, the structure will be:
297    frameworks/support/foo/bar/bar-qux
298
299    Args:
300        group_id: group_id of the new library
301        artifact_id: group_id of the new library
302    """
303    group_id_path = get_group_id_path(group_id)
304    return group_id_path + "/" + artifact_id
305
306def get_package_documentation_file_dir(group_id, artifact_id):
307    """Generates the full package documentation directory
308
309    Given androidx.foo.bar:bar-qux, the structure will be:
310    frameworks/support/foo/bar/bar-qux/src/main/java/androidx/foo/package-info.java
311
312    For Kotlin:
313    frameworks/support/foo/bar/bar-qux/src/main/java/androidx/foo/<group>-<artifact>-documentation.md
314
315    For Compose:
316    frameworks/support/foo/bar/bar-qux/src/commonMain/kotlin/androidx/foo/<group>-<artifact>-documentation.md
317
318    Args:
319        group_id: group_id of the new library
320        artifact_id: group_id of the new library
321    """
322    full_artifact_path = get_full_artifact_path(group_id, artifact_id)
323    if "compose" in group_id:
324        group_id_subpath = "/src/commonMain/kotlin/" + \
325                        group_id.replace(".", "/")
326    else:
327        group_id_subpath = "/src/main/java/" + \
328                        group_id.replace(".", "/")
329    return full_artifact_path + group_id_subpath
330
331def get_package_documentation_filename(group_id, artifact_id, project_type):
332    """Generates the documentation filename
333
334    Given androidx.foo.bar:bar-qux, the structure will be:
335    package-info.java
336
337    or for Kotlin:
338    <group>-<artifact>-documentation.md
339
340    Args:
341        group_id: group_id of the new library
342        artifact_id: group_id of the new library
343        is_kotlin_project: whether or not the library is a kotin project
344    """
345    if project_type == ProjectType.JAVA:
346        return "package-info.java"
347    else:
348        formatted_group_id = group_id.replace(".", "-")
349        return "%s-%s-documentation.md" % (formatted_group_id, artifact_id)
350
351def is_compose_project(group_id, artifact_id):
352    """Returns true if project can be inferred to be a compose / Kotlin project
353    """
354    return  "compose" in group_id or "compose" in artifact_id
355
356def create_directories(group_id, artifact_id, project_type, is_compose_project):
357    """Creates the standard directories for the given group_id and artifact_id.
358
359    Given androidx.foo.bar:bar-qux, the structure will be:
360    frameworks/support/foo/bar/bar-qux/build.gradle
361    frameworks/support/foo/bar/bar-qux/src/main/java/androidx/foo/bar/package-info.java
362    frameworks/support/foo/bar/bar-qux/src/main/java/androidx/foo/bar/artifact-documentation.md
363    frameworks/support/foo/bar/bar-qux/api/current.txt
364
365    Args:
366        group_id: group_id of the new library
367        artifact_id: group_id of the new library
368    """
369    full_artifact_path = get_full_artifact_path(group_id, artifact_id)
370    if not os.path.exists(full_artifact_path):
371        os.makedirs(full_artifact_path)
372
373    # Copy over the OWNERS file if it doesn't exit
374    group_id_path = get_group_id_path(group_id)
375    if not os.path.exists(group_id_path + "/OWNERS"):
376        copyfile(SAMPLE_OWNERS_FP, group_id_path + "/OWNERS")
377
378    # Copy the full src structure, depending on the project source code
379    if is_compose_project:
380        print("Auto-detected Compose project.")
381        cp(SAMPLE_COMPOSE_SRC_FP, full_artifact_path)
382    elif project_type == ProjectType.NATIVE:
383        cp(NATIVE_SRC_FP, full_artifact_path)
384    elif project_type == ProjectType.KOTLIN:
385        cp(SAMPLE_KOTLIN_SRC_FP, full_artifact_path)
386    else:
387        cp(SAMPLE_JAVA_SRC_FP, full_artifact_path)
388
389    # Java only libraries have no dependency on android.
390    # Java-only produces a jar, whereas an android library produces an aar.
391    if (project_type == ProjectType.JAVA and
392            (get_library_type(artifact_id) == "LINT" or
393        ask_yes_or_no("Is this a java-only library? Java-only libraries produce"
394                      " JARs, whereas Android libraries produce AARs."))):
395        sed("com.android.library", "java-library",
396            full_artifact_path + "/build.gradle")
397        sed("org.jetbrains.kotlin.android", "kotlin",
398            full_artifact_path + "/build.gradle")
399
400    # Atomic group Ids have their version configured automatically,
401    # so we can remove the version line from the build file.
402    if is_group_id_atomic(group_id):
403        remove_line("mavenVersion = LibraryVersions.",
404                    full_artifact_path + "/build.gradle")
405
406    # If the project is a library that produces a jar/aar that will go
407    # on GMaven, ask for a special project description.
408    if get_library_type(artifact_id) == "PUBLISHED_LIBRARY":
409        project_description = ask_library_purpose()
410    else:
411        project_description = ask_project_description()
412
413    # Set up the package documentation.
414    full_package_docs_dir = get_package_documentation_file_dir(group_id, artifact_id)
415    package_docs_filename = get_package_documentation_filename(group_id, artifact_id, project_type)
416    full_package_docs_file = os.path.join(full_package_docs_dir, package_docs_filename)
417    # Compose projects use multiple main directories, so we handle it separately
418    if is_compose_project:
419        # Kotlin projects use -documentation.md files, so we need to rename it appropriately.
420        rename_file(full_artifact_path + "/src/commonMain/kotlin/groupId/artifactId-documentation.md",
421                    package_docs_filename)
422        mv_dir(full_artifact_path + "/src/commonMain/kotlin/groupId", full_package_docs_dir)
423    else:
424        if project_type != ProjectType.JAVA:
425            # Kotlin projects use -documentation.md files, so we need to rename it appropriately.
426            # We also rename this file for native projects in case they also have public Kotlin APIs
427            rename_file(full_artifact_path + "/src/main/java/groupId/artifactId-documentation.md",
428                        package_docs_filename)
429        mv_dir(full_artifact_path + "/src/main/java/groupId", full_package_docs_dir)
430
431    # Populate the library type
432    library_type = get_library_type(artifact_id)
433    if project_type == ProjectType.NATIVE and library_type == "PUBLISHED_LIBRARY":
434        library_type = "PUBLISHED_NATIVE_LIBRARY"
435    sed("<LIBRARY_TYPE>", library_type, full_artifact_path + "/build.gradle")
436
437    # Populate the YEAR
438    year = get_year()
439    sed("<YEAR>", year, full_artifact_path + "/build.gradle")
440    sed("<YEAR>", year, full_package_docs_file)
441
442    # Populate the PACKAGE
443    package = generate_package_name(group_id, artifact_id)
444    sed("<PACKAGE>", package, full_package_docs_file)
445    sed("<PACKAGE>", package, full_artifact_path + "/build.gradle")
446
447    # Populate the VERSION macro
448    group_id_version_macro = get_group_id_version_macro(group_id)
449    sed("<GROUPID>", group_id_version_macro, full_artifact_path + "/build.gradle")
450    # Update the name and description in the build.gradle
451    sed("<NAME>", group_id + ":" + artifact_id, full_artifact_path + "/build.gradle")
452    if project_type == ProjectType.NATIVE:
453        sed("<NAME>", artifact_id, full_artifact_path + "/src/main/cpp/CMakeLists.txt")
454        sed("<TARGET>", artifact_id, full_artifact_path + "/build.gradle")
455        create_file(full_artifact_path + "/src/main/cpp/" + artifact_id + ".cpp")
456    sed("<DESCRIPTION>", project_description, full_artifact_path + "/build.gradle")
457
458
459def get_new_settings_gradle_line(group_id, artifact_id):
460    """Generates the line needed for frameworks/support/settings.gradle.
461
462    For a library androidx.foo.bar:bar-qux, the new gradle command will be
463    the form:
464    ./gradlew :foo:bar:bar-qux:<command>
465
466    We special case on compose that we can properly populate the build type
467    of either MAIN or COMPOSE.
468
469    Args:
470        group_id: group_id of the new library
471        artifact_id: group_id of the new library
472    """
473
474    build_type = "MAIN"
475    if is_compose_project(group_id, artifact_id):
476        build_type = "COMPOSE"
477
478    gradle_cmd = get_gradle_project_coordinates(group_id, artifact_id)
479    return "includeProject(\"" + gradle_cmd + "\", [BuildType." + build_type + "])\n"
480
481def update_settings_gradle(group_id, artifact_id):
482    """Updates frameworks/support/settings.gradle with the new library.
483
484    Args:
485        group_id: group_id of the new library
486        artifact_id: group_id of the new library
487    """
488    # Open file for reading and get all lines
489    with open(SETTINGS_GRADLE_FP, 'r') as f:
490        settings_gradle_lines = f.readlines()
491    num_lines = len(settings_gradle_lines)
492
493    new_settings_gradle_line = get_new_settings_gradle_line(group_id, artifact_id)
494    for i in range(num_lines):
495        cur_line = settings_gradle_lines[i]
496        if "includeProject" not in cur_line:
497            continue
498        # Iterate through until you found the alphabetical place to insert the new line
499        if new_settings_gradle_line <= cur_line:
500            insert_line = i
501            break
502        else:
503            insert_line = i + 1
504    settings_gradle_lines.insert(insert_line, new_settings_gradle_line)
505
506    # Open file for writing and update all lines
507    with open(SETTINGS_GRADLE_FP, 'w') as f:
508        f.writelines(settings_gradle_lines)
509
510def get_new_docs_tip_of_tree_build_grade_line(group_id, artifact_id):
511    """Generates the line needed for docs-tip-of-tree/build.gradle.
512
513    For a library androidx.foo.bar:bar-qux, the new line will be of the form:
514    docs(project(":foo:bar:bar-qux"))
515
516    If it is a sample project, then it will return None. samples(project(":foo:bar:bar-qux-sample")) needs to be added to the androidx block of the library build.gradle file.
517
518    Args:
519        group_id: group_id of the new library
520        artifact_id: group_id of the new library
521    """
522
523    gradle_cmd = get_gradle_project_coordinates(group_id, artifact_id)
524    prefix = "docs"
525    if "sample" in gradle_cmd:
526        print("Auto-detected sample project. Please add the sample dependency to androidx block of the library build.gradle file. See compose/ui/ui/build.gradle for an example.")
527        return None
528    return "    %s(project(\"%s\"))\n" % (prefix, gradle_cmd)
529
530def update_docs_tip_of_tree_build_grade(group_id, artifact_id):
531    """Updates docs-tip-of-tree/build.gradle with the new library.
532
533    We ask for confirmation if the library contains either "benchmark"
534    or "test".
535
536    Args:
537        group_id: group_id of the new library
538        artifact_id: group_id of the new library
539    """
540    # Confirm with user that we want to generate docs for anything
541    # that might be a test or a benchmark.
542    if ("test" in group_id or "test" in artifact_id
543        or "benchmark" in group_id or "benchmark" in artifact_id):
544        if not ask_yes_or_no(("Should tip-of-tree documentation be generated "
545                              "for project %s:%s?" % (group_id, artifact_id))):
546            return
547
548    # Open file for reading and get all lines
549    with open(DOCS_TOT_BUILD_GRADLE_FP, 'r') as f:
550        docs_tot_bg_lines = f.readlines()
551    index_of_real_dependencies_block = next(
552        idx for idx, line in enumerate(docs_tot_bg_lines) if line.startswith("dependencies {")
553    )
554    if (index_of_real_dependencies_block == None):
555        raise RuntimeError("Couldn't find dependencies block")
556    num_lines = len(docs_tot_bg_lines)
557
558    new_docs_tot_bq_line = get_new_docs_tip_of_tree_build_grade_line(group_id, artifact_id)
559    for i in range(index_of_real_dependencies_block, num_lines):
560        cur_line = docs_tot_bg_lines[i]
561        if "project" not in cur_line:
562            continue
563        # Iterate through until you found the alphabetical place to insert the new line
564        if new_docs_tot_bq_line.split("project")[1] <= cur_line.split("project")[1]:
565            insert_line = i
566            break
567        else:
568            insert_line = i + 1
569    docs_tot_bg_lines.insert(insert_line, new_docs_tot_bq_line)
570
571    # Open file for writing and update all lines
572    with open(DOCS_TOT_BUILD_GRADLE_FP, 'w') as f:
573        f.writelines(docs_tot_bg_lines)
574
575
576def insert_new_group_id_into_library_versions_toml(group_id):
577    """Inserts a group ID into the libraryversions.toml file.
578
579    If one already exists, then this function just returns and reuses
580    the existing one.
581
582    Args:
583        group_id: group_id of the new library
584    """
585    new_group_id_variable_name = group_id.replace("androidx.","").replace(".","_").upper()
586
587    # Open toml file
588    library_versions = toml.load(LIBRARY_VERSIONS_FP, decoder=toml.TomlPreserveCommentDecoder())
589    if not new_group_id_variable_name in library_versions["versions"]:
590        library_versions["versions"][new_group_id_variable_name] = "1.0.0-alpha01"
591    if not new_group_id_variable_name in library_versions["groups"]:
592        decoder = toml.decoder.TomlDecoder()
593        group_entry = decoder.get_empty_inline_table()
594        group_entry["group"] = group_id
595        group_entry["atomicGroupVersion"] = "versions." + new_group_id_variable_name
596        library_versions["groups"][new_group_id_variable_name] = group_entry
597
598    # Sort the entries
599    library_versions["versions"] = dict(sorted(library_versions["versions"].items()))
600    library_versions["groups"] = dict(sorted(library_versions["groups"].items()))
601
602    # Open file for writing and update toml
603    with open(LIBRARY_VERSIONS_FP, 'w') as f:
604        # Encoder arg enables preservation of inline dicts.
605        versions_toml_file_string = toml.dumps(library_versions,
606                                               encoder=toml.TomlPreserveCommentEncoder(preserve=True))
607        versions_toml_file_string_new = re.sub(",]", " ]", versions_toml_file_string)
608        versions_toml_file_string_new
609        f.write(versions_toml_file_string_new)
610
611
612def is_group_id_atomic(group_id):
613    """Checks if a group ID is atomic using the libraryversions.toml file.
614
615    If one already exists, then this function evaluates the group id
616    and returns the appropriate atomicity.  Otherwise, it returns
617    False.
618
619    Example of an atomic library group:
620        ACTIVITY = { group = "androidx.work", atomicGroupVersion = "WORK" }
621    Example of a non-atomic library group:
622        WEAR = { group = "androidx.wear" }
623
624    Args:
625        group_id: group_id of the library we're checking.
626    """
627    library_versions = toml.load(LIBRARY_VERSIONS_FP)
628    for library_group in library_versions["groups"]:
629      if group_id == library_versions["groups"][library_group]["group"]:
630          return "atomicGroupVersion" in library_versions["groups"][library_group]
631
632    return False
633
634
635def print_todo_list(group_id, artifact_id, project_type):
636    """Prints to the todo list once the script has finished.
637
638    There are some pieces that can not be automated or require human eyes.
639    List out the appropriate todos so that the users knows what needs
640    to be done prior to uploading.
641
642    Args:
643        group_id: group_id of the new library
644        artifact_id: group_id of the new library
645    """
646    build_gradle_path = get_full_artifact_path(group_id, artifact_id) + \
647                        "/build.gradle"
648    owners_file_path = get_group_id_path(group_id) + "/OWNERS"
649    package_docs_path = os.path.join(
650        get_package_documentation_file_dir(group_id, artifact_id),
651        get_package_documentation_filename(group_id, artifact_id, project_type))
652    print("---\n")
653    print("Created the project.  The following TODOs need to be completed by "
654          "you:\n")
655    print("\t1. Check that the OWNERS file is in the correct place. It is "
656          "currently at:"
657          "\n\t\t" + owners_file_path)
658    print("\t2. Add your name (and others) to the OWNERS file:" + \
659          "\n\t\t" + owners_file_path)
660    print("\t3. Check that the correct library version is assigned in the "
661          "build.gradle:"
662          "\n\t\t" + build_gradle_path)
663    print("\t4. Fill out the project/module name in the build.gradle:"
664          "\n\t\t" + build_gradle_path)
665    print("\t5. Update the project/module package documentation:"
666          "\n\t\t" + package_docs_path)
667
668def main(args):
669    # Parse arguments and check for existence of build ID or file
670    args = parser.parse_args()
671    if not args.group_id or not args.artifact_id:
672        parser.error("You must specify a group_id and an artifact_id")
673        sys.exit(1)
674    if not validate_name(args.group_id, args.artifact_id):
675        sys.exit(1)
676    if is_compose_project(args.group_id, args.artifact_id):
677        project_type = ProjectType.KOTLIN
678    else:
679        project_type = ask_project_type()
680    insert_new_group_id_into_library_versions_toml(
681        args.group_id
682    )
683    create_directories(
684        args.group_id,
685        args.artifact_id,
686        project_type,
687        is_compose_project(args.group_id, args.artifact_id)
688    )
689    update_settings_gradle(args.group_id, args.artifact_id)
690    update_docs_tip_of_tree_build_grade(args.group_id, args.artifact_id)
691    print("Created directories. \nRunning updateApi for the new "
692          "library, this may take a minute...", end='')
693    if run_update_api(args.group_id, args.artifact_id):
694        print("done.")
695    else:
696        print("failed.  Please investigate manually.")
697    print_todo_list(args.group_id, args.artifact_id, project_type)
698
699if __name__ == '__main__':
700    main(sys.argv)
701