1 # This Source Code Form is subject to the terms of the Mozilla Public
2 # License, v. 2.0. If a copy of the MPL was not distributed with this
3 # file, You can obtain one at http://mozilla.org/MPL/2.0/.
13 import mozpack
.path
as mozpath
14 from mach
.decorators
import Command
, CommandArgument
, SubCommand
15 from mozbuild
.base
import MachCommandConditions
as conditions
16 from mozbuild
.shellutil
import split
as shell_split
17 from mozfile
import which
19 # Mach's conditions facility doesn't support subcommands. Print a
20 # deprecation message ourselves instead.
21 LINT_DEPRECATION_MESSAGE
= """
22 Android lints are now integrated with mozlint. Instead of
23 `mach android {api-lint,checkstyle,lint,test}`, run
24 `mach lint --linter android-{api-lint,checkstyle,lint,test}`.
29 # NOTE python/mach/mach/commands/commandinfo.py references this function
30 # by name. If this function is renamed or removed, that file should
31 # be updated accordingly as well.
33 """Command no longer exists! Use the Gradle configuration rooted in the top source directory
36 See https://developer.mozilla.org/en-US/docs/Simple_Firefox_for_Android_build#Developing_Firefox_for_Android_in_Android_Studio_or_IDEA_IntelliJ. # NOQA: E501
44 description
="Run Android-specific commands.",
45 conditions
=[conditions
.is_android
],
47 def android(command_context
):
54 """Assemble Firefox for Android.
55 See http://firefox-source-docs.mozilla.org/build/buildsystem/toolchains.html#firefox-for-android-with-gradle""", # NOQA: E501
57 @CommandArgument("args", nargs
=argparse
.REMAINDER
)
58 def android_assemble_app(command_context
, args
):
61 command_context
.substs
["GRADLE_ANDROID_APP_TASKS"] + ["-x", "lint"] + args
,
70 "generate-sdk-bindings",
71 """Generate SDK bindings used when building GeckoView.""",
76 help="config files, like [/path/to/ClassName-classes.txt]+",
78 @CommandArgument("args", nargs
=argparse
.REMAINDER
)
79 def android_generate_sdk_bindings(command_context
, inputs
, args
):
83 # Turn "/path/to/ClassName-classes.txt" into "ClassName".
84 return os
.path
.basename(input).rsplit("-classes.txt", 1)[0]
86 bindings_inputs
= list(itertools
.chain(*((input, stem(input)) for input in inputs
)))
87 bindings_args
= "-Pgenerate_sdk_bindings_args={}".format(";".join(bindings_inputs
))
91 command_context
.substs
["GRADLE_ANDROID_GENERATE_SDK_BINDINGS_TASKS"]
102 "generate-generated-jni-wrappers",
103 """Generate GeckoView JNI wrappers used when building GeckoView.""",
105 @CommandArgument("args", nargs
=argparse
.REMAINDER
)
106 def android_generate_generated_jni_wrappers(command_context
, args
):
109 command_context
.substs
["GRADLE_ANDROID_GENERATE_GENERATED_JNI_WRAPPERS_TASKS"]
120 """Run Android api-lint.
121 REMOVED/DEPRECATED: Use 'mach lint --linter android-api-lint'.""",
123 def android_apilint_REMOVED(command_context
):
124 print(LINT_DEPRECATION_MESSAGE
)
132 REMOVED/DEPRECATED: Use 'mach lint --linter android-test'.""",
134 def android_test_REMOVED(command_context
):
135 print(LINT_DEPRECATION_MESSAGE
)
143 REMOVED/DEPRECATED: Use 'mach lint --linter android-lint'.""",
145 def android_lint_REMOVED(command_context
):
146 print(LINT_DEPRECATION_MESSAGE
)
153 """Run Android checkstyle.
154 REMOVED/DEPRECATED: Use 'mach lint --linter android-checkstyle'.""",
156 def android_checkstyle_REMOVED(command_context
):
157 print(LINT_DEPRECATION_MESSAGE
)
163 "gradle-dependencies",
164 """Collect Android Gradle dependencies.
165 See http://firefox-source-docs.mozilla.org/build/buildsystem/toolchains.html#firefox-for-android-with-gradle""", # NOQA: E501
167 @CommandArgument("args", nargs
=argparse
.REMAINDER
)
168 def android_gradle_dependencies(command_context
, args
):
169 # We don't want to gate producing dependency archives on clean
170 # lint or checkstyle, particularly because toolchain versions
171 # can change the outputs for those processes.
174 command_context
.substs
["GRADLE_ANDROID_DEPENDENCIES_TASKS"]
183 def get_maven_archive_paths(maven_folder
):
184 for subdir
, _
, files
in os
.walk(maven_folder
):
185 if "-SNAPSHOT" in subdir
:
188 yield os
.path
.join(subdir
, file)
191 def create_maven_archive(topobjdir
):
192 gradle_folder
= os
.path
.join(topobjdir
, "gradle")
193 maven_folder
= os
.path
.join(gradle_folder
, "maven")
195 # Create the archive, with no compression: The archive contents are large
196 # files which cannot be significantly compressed; attempting to compress
197 # the archive is usually expensive in time and results in minimal
199 # Even though the archive is not compressed, use the .xz file extension
200 # so that the taskcluster worker also skips compression.
201 with tarfile
.open(os
.path
.join(gradle_folder
, "target.maven.tar.xz"), "w") as tar
:
202 for abs_path
in get_maven_archive_paths(maven_folder
):
205 arcname
=os
.path
.join(
206 "geckoview", os
.path
.relpath(abs_path
, maven_folder
)
214 """Create GeckoView archives.
215 See http://firefox-source-docs.mozilla.org/build/buildsystem/toolchains.html#firefox-for-android-with-gradle""", # NOQA: E501
217 @CommandArgument("args", nargs
=argparse
.REMAINDER
)
218 def android_archive_geckoview(command_context
, args
):
221 command_context
.substs
["GRADLE_ANDROID_ARCHIVE_GECKOVIEW_TASKS"] + args
,
227 if "MOZ_AUTOMATION" in os
.environ
:
228 create_maven_archive(command_context
.topobjdir
)
233 @SubCommand("android", "build-geckoview_example", """Build geckoview_example """)
234 @CommandArgument("args", nargs
=argparse
.REMAINDER
)
235 def android_build_geckoview_example(command_context
, args
):
238 command_context
.substs
["GRADLE_ANDROID_BUILD_GECKOVIEW_EXAMPLE_TASKS"] + args
,
243 "Execute `mach android install-geckoview_example` "
244 "to push the geckoview_example and test APKs to a device."
250 @SubCommand("android", "compile-all", """Build all source files""")
251 @CommandArgument("args", nargs
=argparse
.REMAINDER
)
252 def android_compile_all(command_context
, args
):
255 command_context
.substs
["GRADLE_ANDROID_COMPILE_ALL_TASKS"] + args
,
262 def install_app_bundle(command_context
, bundle
):
263 from mozdevice
import ADBDeviceFactory
265 bundletool
= mozpath
.join(command_context
._mach
_context
.state_dir
, "bundletool.jar")
266 device
= ADBDeviceFactory(verbose
=True)
267 bundle_path
= mozpath
.join(command_context
.topobjdir
, bundle
)
268 java_home
= java_home
= os
.path
.dirname(
269 os
.path
.dirname(command_context
.substs
["JAVA"])
271 device
.install_app_bundle(bundletool
, bundle_path
, java_home
, timeout
=120)
274 @SubCommand("android", "install-geckoview_example", """Install geckoview_example """)
275 @CommandArgument("args", nargs
=argparse
.REMAINDER
)
276 def android_install_geckoview_example(command_context
, args
):
279 command_context
.substs
["GRADLE_ANDROID_INSTALL_GECKOVIEW_EXAMPLE_TASKS"] + args
,
284 "Execute `mach android build-geckoview_example` "
285 "to just build the geckoview_example and test APKs."
292 "android", "install-geckoview-test_runner", """Install geckoview.test_runner """
294 @CommandArgument("args", nargs
=argparse
.REMAINDER
)
295 def android_install_geckoview_test_runner(command_context
, args
):
298 command_context
.substs
["GRADLE_ANDROID_INSTALL_GECKOVIEW_TEST_RUNNER_TASKS"]
307 "install-geckoview-test_runner-aab",
308 """Install geckoview.test_runner with AAB""",
310 @CommandArgument("args", nargs
=argparse
.REMAINDER
)
311 def android_install_geckoview_test_runner_aab(command_context
, args
):
314 command_context
.substs
["GRADLE_ANDROID_GECKOVIEW_TEST_RUNNER_BUNDLE"],
321 "install-geckoview_example-aab",
322 """Install geckoview_example with AAB""",
324 @CommandArgument("args", nargs
=argparse
.REMAINDER
)
325 def android_install_geckoview_example_aab(command_context
, args
):
328 command_context
.substs
["GRADLE_ANDROID_GECKOVIEW_EXAMPLE_BUNDLE"],
333 @SubCommand("android", "install-geckoview-test", """Install geckoview.test """)
334 @CommandArgument("args", nargs
=argparse
.REMAINDER
)
335 def android_install_geckoview_test(command_context
, args
):
338 command_context
.substs
["GRADLE_ANDROID_INSTALL_GECKOVIEW_TEST_TASKS"] + args
,
347 """Create GeckoView javadoc and optionally upload to Github""",
349 @CommandArgument("--archive", action
="store_true", help="Generate a javadoc archive.")
353 help="Upload geckoview documentation to Github, using the specified USER/REPO.",
357 metavar
="BRANCH[/PATH]",
359 help="Use the specified branch/path for documentation commits.",
365 help="Use the specified path for javadoc commits.",
370 default
="GeckoView docs upload",
371 help="Use the specified message for commits.",
373 def android_geckoview_docs(
382 command_context
.substs
["GRADLE_ANDROID_GECKOVIEW_DOCS_ARCHIVE_TASKS"]
384 else command_context
.substs
["GRADLE_ANDROID_GECKOVIEW_DOCS_TASKS"]
387 ret
= gradle(command_context
, tasks
, verbose
=True)
388 if ret
or not upload
:
393 "level": os
.environ
.get("MOZ_SCM_LEVEL", "0"),
394 "project": os
.environ
.get("MH_BRANCH", "unknown"),
395 "revision": os
.environ
.get("GECKO_HEAD_REV", "tip"),
399 # In order to push to GitHub from TaskCluster, we store a private key
400 # in the TaskCluster secrets store in the format {"content": "<KEY>"},
401 # and the corresponding public key as a writable deploy key for the
402 # destination repo on GitHub.
403 secret
= os
.environ
.get("GECKOVIEW_DOCS_UPLOAD_SECRET", "").format(**fmt
)
405 # Set up a private key from the secrets store if applicable.
408 req
= requests
.get("http://taskcluster/secrets/v1/secret/" + secret
)
409 req
.raise_for_status()
411 keyfile
= mozpath
.abspath("gv-docs-upload-key")
412 with
open(keyfile
, "w") as f
:
413 os
.chmod(keyfile
, 0o600)
414 f
.write(req
.json()["secret"]["content"])
416 # Turn off strict host key checking so ssh does not complain about
417 # unknown github.com host. We're not pushing anything sensitive, so
418 # it's okay to not check GitHub's host keys.
419 env
["GIT_SSH_COMMAND"] = 'ssh -i "%s" -o StrictHostKeyChecking=no' % keyfile
422 branch
= upload_branch
.format(**fmt
)
423 repo_url
= "git@github.com:%s.git" % upload
424 repo_path
= mozpath
.abspath("gv-docs-repo")
425 command_context
.run_process(
439 env
["GIT_DIR"] = mozpath
.join(repo_path
, ".git")
440 env
["GIT_WORK_TREE"] = repo_path
441 env
["GIT_AUTHOR_NAME"] = env
["GIT_COMMITTER_NAME"] = "GeckoView Docs Bot"
442 env
["GIT_AUTHOR_EMAIL"] = env
["GIT_COMMITTER_EMAIL"] = "nobody@mozilla.com"
444 # Copy over user documentation.
447 # Extract new javadoc to specified directory inside repo.
448 src_tar
= mozpath
.join(
449 command_context
.topobjdir
,
456 "geckoview-javadoc.jar",
458 dst_path
= mozpath
.join(repo_path
, javadoc_path
.format(**fmt
))
459 mozfile
.remove(dst_path
)
460 mozfile
.extract_zip(src_tar
, dst_path
)
463 command_context
.run_process(["git", "add", "--all"], append_env
=env
, pass_thru
=True)
465 command_context
.run_process(
466 ["git", "diff", "--cached", "--quiet"],
469 ensure_exit_code
=False,
473 # We have something to commit.
474 command_context
.run_process(
475 ["git", "commit", "--message", upload_message
.format(**fmt
)],
479 command_context
.run_process(
480 ["git", "push", "origin", branch
], append_env
=env
, pass_thru
=True
483 mozfile
.remove(repo_path
)
485 mozfile
.remove(keyfile
)
492 description
="Run gradle.",
493 conditions
=[conditions
.is_android
],
499 help="Verbose output for what commands the build is running.",
501 @CommandArgument("args", nargs
=argparse
.REMAINDER
)
502 def gradle(command_context
, args
, verbose
=False):
504 # Avoid logging the command
505 command_context
.log_manager
.terminal_handler
.setLevel(logging
.CRITICAL
)
507 # In automation, JAVA_HOME is set via mozconfig, which needs
508 # to be specially handled in each mach command. This turns
509 # $JAVA_HOME/bin/java into $JAVA_HOME.
510 java_home
= os
.path
.dirname(os
.path
.dirname(command_context
.substs
["JAVA"]))
512 gradle_flags
= command_context
.substs
.get("GRADLE_FLAGS", "") or os
.environ
.get(
515 gradle_flags
= shell_split(gradle_flags
)
517 # We force the Gradle JVM to run with the UTF-8 encoding, since we
518 # filter strings.xml, which is really UTF-8; the ellipsis character is
519 # replaced with ??? in some encodings (including ASCII). It's not yet
520 # possible to filter with encodings in Gradle
521 # (https://github.com/gradle/gradle/pull/520) and it's challenging to
522 # do our filtering with Gradle's Ant support. Moreover, all of the
523 # Android tools expect UTF-8: see
524 # http://tools.android.com/knownissues/encoding. See
525 # http://stackoverflow.com/a/21267635 for discussion of this approach.
527 # It's not even enough to set the encoding just for Gradle; it
528 # needs to be for JVMs spawned by Gradle as well. This
529 # happens during the maven deployment generating the GeckoView
530 # documents; this works around "error: unmappable character
531 # for encoding ASCII" in exoplayer2. See
532 # https://discuss.gradle.org/t/unmappable-character-for-encoding-ascii-when-building-a-utf-8-project/10692/11 # NOQA: E501
533 # and especially https://stackoverflow.com/a/21755671.
535 if command_context
.substs
.get("MOZ_AUTOMATION"):
536 gradle_flags
+= ["--console=plain"]
538 env
= os
.environ
.copy()
541 "GRADLE_OPTS": "-Dfile.encoding=utf-8",
542 "JAVA_HOME": java_home
,
543 "JAVA_TOOL_OPTIONS": "-Dfile.encoding=utf-8",
544 # Let Gradle get the right Python path on Windows
545 "GRADLE_MACH_PYTHON": sys
.executable
,
548 # Set ANDROID_SDK_ROOT if --with-android-sdk was set.
549 # See https://bugzilla.mozilla.org/show_bug.cgi?id=1576471
550 android_sdk_root
= command_context
.substs
.get("ANDROID_SDK_ROOT", "")
552 env
["ANDROID_SDK_ROOT"] = android_sdk_root
554 return command_context
.run_process(
555 [command_context
.substs
["GRADLE"]] + gradle_flags
+ args
,
557 pass_thru
=True, # Allow user to run gradle interactively.
558 ensure_exit_code
=False, # Don't throw on non-zero exit code.
559 cwd
=mozpath
.join(command_context
.topsrcdir
),
563 @Command("gradle-install", category
="devenv", conditions
=[REMOVED
])
564 def gradle_install_REMOVED(command_context
):
572 description
="Run the Android emulator with an AVD from test automation. "
573 "Environment variable MOZ_EMULATOR_COMMAND_ARGS, if present, will "
574 "over-ride the command line arguments used to launch the emulator.",
579 choices
=["arm", "arm64", "x86_64"],
580 help="Specify which AVD to run in emulator. "
581 'One of "arm" (Android supporting armv7 binaries), '
582 '"arm64" (for Apple Silicon), or '
583 '"x86_64" (Android supporting x86 or x86_64 binaries, '
584 "recommended for most applications). "
585 "By default, the value will match the current build environment.",
587 @CommandArgument("--wait", action
="store_true", help="Wait for emulator to be closed.")
588 @CommandArgument("--gpu", help="Over-ride the emulator -gpu argument.")
590 "--verbose", action
="store_true", help="Log informative status messages."
600 Run the Android emulator with one of the AVDs used in the Mozilla
601 automated test environment. If necessary, the AVD is fetched from
602 the taskcluster server and installed.
604 from mozrunner
.devices
.android_device
import AndroidEmulator
606 emulator
= AndroidEmulator(
609 substs
=command_context
.substs
,
610 device_serial
="emulator-5554",
612 if emulator
.is_running():
613 # It is possible to run multiple emulators simultaneously, but:
614 # - if more than one emulator is using the same avd, errors may
615 # occur due to locked resources;
616 # - additional parameters must be specified when running tests,
617 # to select a specific device.
618 # To avoid these complications, allow just one emulator at a time.
623 "An Android emulator is already running.\n"
624 "Close the existing emulator and re-run this command.",
628 if not emulator
.check_avd():
633 "AVD not found. Please run |mach bootstrap|.",
637 if not emulator
.is_available():
642 "Emulator binary not found.\n"
643 "Install the Android SDK and make sure 'emulator' is in your PATH.",
651 "Starting Android emulator running %s..." % emulator
.get_avd_description(),
654 if emulator
.wait_for_start():
656 logging
.INFO
, "emulator", {}, "Android emulator is running."
659 # This is unusual but the emulator may still function.
664 "Unable to verify that emulator is running.",
667 if conditions
.is_android(command_context
):
672 "Use 'mach install' to install or update Firefox on your emulator.",
679 "No Firefox for Android build detected.\n"
680 "Switch to a Firefox for Android build context or use 'mach bootstrap'\n"
681 "to setup an Android build environment.",
686 logging
.INFO
, "emulator", {}, "Waiting for Android emulator to close..."
694 "Android emulator completed with return code %d." % rc
,
701 "Unable to retrieve Android emulator return code.",
709 description
="Uplift patch to https://github.com/mozilla-mobile/firefox-android.",
715 help="Revision or revisions to uplift. Supported values are the same as what your "
716 "VCS provides. Defaults to the current HEAD revision.",
719 "firefox_android_clone_dir",
720 help="The directory where your local clone of "
721 "https://github.com/mozilla-mobile/firefox-android repo is.",
726 firefox_android_clone_dir
,
728 revset
= _get_default_revset_if_needed(command_context
, revset
)
729 major_version
= _get_major_version(command_context
, revset
)
730 uplift_version
= major_version
- 1
731 bug_number
= _get_bug_number(command_context
, revset
)
733 f
"{uplift_version}-bug{bug_number}" if bug_number
else f
"{uplift_version}-nobug"
739 "new_branch_name": new_branch_name
,
740 "firefox_android_clone_dir": firefox_android_clone_dir
,
742 "Creating branch {new_branch_name} in {firefox_android_clone_dir}...",
746 _checkout_new_branch_updated_to_the_latest_remote(
747 command_context
, firefox_android_clone_dir
, uplift_version
, new_branch_name
749 _export_and_apply_revset(command_context
, revset
, firefox_android_clone_dir
)
750 except subprocess
.CalledProcessError
:
756 {"revset": revset
, "firefox_android_clone_dir": firefox_android_clone_dir
},
757 "Revision(s) {revset} now applied to {firefox_android_clone_dir}. Please go to "
758 "this directory, inspect the commit(s), and push.",
763 def _get_default_revset_if_needed(command_context
, revset
):
764 if revset
is not None:
766 if conditions
.is_hg(command_context
):
768 if conditions
.is_git(command_context
):
770 raise NotImplementedError()
773 def _get_major_version(command_context
, revset
):
774 milestone_txt
= _get_milestone_txt(command_context
, revset
)
775 version
= _extract_version_from_milestone_txt(milestone_txt
)
776 return _extract_major_version(version
)
779 def _get_bug_number(command_context
, revision
):
780 revision_message
= _extract_revision_message(command_context
, revision
)
781 return _extract_bug_number(revision_message
)
784 def _get_milestone_txt(command_context
, revset
):
785 if conditions
.is_hg(command_context
):
791 "config/milestone.txt",
793 elif conditions
.is_git(command_context
):
794 revision
= revset
.split("..")[-1]
798 f
"{revision}:config/milestone.txt",
801 raise NotImplementedError()
803 return subprocess
.check_output(args
, text
=True)
806 def _extract_version_from_milestone_txt(milestone_txt
):
807 return milestone_txt
.splitlines()[-1]
810 def _extract_major_version(version
):
811 return int(version
.split(".")[0])
814 def _extract_revision_message(command_context
, revision
):
815 if conditions
.is_hg(command_context
):
820 f
"first({revision})",
824 elif conditions
.is_git(command_context
):
834 raise NotImplementedError()
836 return subprocess
.check_output(args
, text
=True)
839 # Source: https://hg.mozilla.org/hgcustom/version-control-tools/file/cef43d3d676e9f9e9668a50a5d90c012e4025e5b/pylib/mozautomation/mozautomation/commitparser.py#l12
840 _BUG_TEMPLATE
= re
.compile(
841 r
"""# bug followed by any sequence of numbers, or
842 # a standalone sequence of numbers
847 # a sequence of 5+ numbers preceded by whitespace
849 # numbers at the very beginning
852 (?:\s*\#?)(\d+)(?=\b)
858 def _extract_bug_number(revision_message
):
860 return _BUG_TEMPLATE
.match(revision_message
).group(2)
861 except AttributeError:
865 _FIREFOX_ANDROID_URL
= "https://github.com/mozilla-mobile/firefox-android"
868 def _checkout_new_branch_updated_to_the_latest_remote(
869 command_context
, firefox_android_clone_dir
, uplift_version
, new_branch_name
874 _FIREFOX_ANDROID_URL
,
875 f
"+releases_v{uplift_version}:{new_branch_name}",
879 subprocess
.check_call(args
, cwd
=firefox_android_clone_dir
)
880 except subprocess
.CalledProcessError
:
885 "firefox_android_clone_dir": firefox_android_clone_dir
,
886 "new_branch_name": new_branch_name
,
888 "Could not fetch branch {new_branch_name}. This may be a network issue. If "
889 "not, please go to {firefox_android_clone_dir}, inspect the branch and "
890 "delete it (with `git branch -D {new_branch_name}`) if you don't have any "
891 "use for it anymore",
900 subprocess
.check_call(args
, cwd
=firefox_android_clone_dir
)
903 _MERCURIAL_REVISION_TO_GIT_COMMIT_TEMPLATE
= """
904 From 1234567890abcdef1234567890abcdef12345678 Sat Jan 1 00:00:00 2000
906 Date: {date|rfc822date}
912 def _export_and_apply_revset(command_context
, revset
, firefox_android_clone_dir
):
913 export_command
, import_command
= _get_export_import_commands(
914 command_context
, revset
917 export_process
= subprocess
.Popen(export_command
, stdout
=subprocess
.PIPE
)
919 subprocess
.check_call(
920 import_command
, stdin
=export_process
.stdout
, cwd
=firefox_android_clone_dir
922 except subprocess
.CalledProcessError
:
926 {"firefox_android_clone_dir": firefox_android_clone_dir
},
927 "Could not run `git am`. Please go to {firefox_android_clone_dir} and fix "
928 "the conflicts. Then run `git am --continue`.",
931 export_process
.wait()
934 def _get_export_import_commands(command_context
, revset
):
935 if conditions
.is_hg(command_context
):
943 _MERCURIAL_REVISION_TO_GIT_COMMIT_TEMPLATE
,
947 import_command
= [str(which("git")), "am", "-p3"]
948 elif conditions
.is_git(command_context
):
952 "--relative=mobile/android",
955 # From the git man page:
956 # > If you want to format only <commit> itself, you can do this with
957 # > git format-patch -1 <commit>."
959 # https://git-scm.com/docs/git-format-patch#_description
960 if _is_single_revision(command_context
, revset
):
961 export_command
.append("-1")
963 export_command
.extend(
969 import_command
= [str(which("git")), "am"]
971 raise NotImplementedError()
973 return export_command
, import_command
976 def _is_single_revision(command_context
, revset
):
977 if conditions
.is_git(command_context
):
987 raise NotImplementedError()
989 revisions
= subprocess
.check_output(command
, text
=True)
990 return len(revisions
.splitlines()) == 1