blob: 9ecae0e18233b1192ac530ceb24cc81fb63f7987 [file] [log] [blame]
Wyatt Heplerb16dfd32020-10-12 08:46:38 -07001# Copyright 2020 The Pigweed Authors
2#
3# Licensed under the Apache License, Version 2.0 (the "License"); you may not
4# use this file except in compliance with the License. You may obtain a copy of
5# the License at
6#
7# https://www.apache.org/licenses/LICENSE-2.0
8#
9# Unless required by applicable law or agreed to in writing, software
10# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
11# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
12# License for the specific language governing permissions and limitations under
13# the License.
14
15import("//build_overrides/pigweed.gni")
16
Wyatt Heplerb8e13602020-10-19 11:20:36 -070017import("$dir_pw_build/input_group.gni")
18import("$dir_pw_build/python_action.gni")
Wyatt Heplerb16dfd32020-10-12 08:46:38 -070019
Wyatt Hepler438caa02021-01-15 17:13:11 -080020# Python packages provide the following targets as $target_name.$subtarget.
21_python_subtargets = [
22 "tests",
23 "lint",
24 "lint.mypy",
25 "lint.pylint",
26 "install",
27 "wheel",
28]
29
Wyatt Heplerb16dfd32020-10-12 08:46:38 -070030# Defines a Python package. GN Python packages contain several GN targets:
31#
32# - $name - Provides the Python files in the build, but does not take any
33# actions. All subtargets depend on this target.
34# - $name.lint - Runs static analyis tools on the Python code. This is a group
35# of two subtargets:
Wyatt Hepler8ce90132020-12-03 10:57:20 -080036# - $name.lint.mypy - Runs mypy (if enabled).
37# - $name.lint.pylint - Runs pylint (if enabled).
Wyatt Heplerb16dfd32020-10-12 08:46:38 -070038# - $name.tests - Runs all tests for this package.
Rob Mohrfcf60372020-11-04 11:41:36 -080039# - $name.install - Installs the package in a venv.
Wyatt Heplerb16dfd32020-10-12 08:46:38 -070040# - $name.wheel - Builds a Python wheel for the package. (Not implemented.)
41#
Wyatt Hepler438caa02021-01-15 17:13:11 -080042# All Python packages are instantiated with the default toolchain, regardless of
43# the current toolchain.
44#
Wyatt Hepler8ce90132020-12-03 10:57:20 -080045# TODO(pwbug/239): Implement wheel building.
Wyatt Heplerb16dfd32020-10-12 08:46:38 -070046#
47# Args:
48# setup: List of setup file paths (setup.py or pyproject.toml & setup.cfg),
49# which must all be in the same directory.
50# sources: Python sources files in the package.
51# tests: Test files for this Python package.
52# python_deps: Dependencies on other pw_python_packages in the GN build.
Wyatt Hepler620b3e02021-01-22 09:14:45 -080053# python_test_deps: Test-only pw_python_package dependencies.
Wyatt Heplerb16dfd32020-10-12 08:46:38 -070054# other_deps: Dependencies on GN targets that are not pw_python_packages.
55# inputs: Other files to track, such as package_data.
Wyatt Hepler8ce90132020-12-03 10:57:20 -080056# lint: If true (default), applies mypy and pylint to the package. If false,
57# does not.
58# pylintrc: Optional path to a pylintrc configuration file to use. If not
59# provided, Pylint's default rcfile search is used. Pylint is executed
60# from the package's setup directory, so pylintrc files in that directory
61# will take precedence over others.
62# mypy_ini: Optional path to a mypy configuration file to use. If not
63# provided, mypy's default configuration file search is used. mypy is
64# executed from the package's setup directory, so mypy.ini files in that
65# directory will take precedence over others.
Wyatt Heplerb16dfd32020-10-12 08:46:38 -070066template("pw_python_package") {
Wyatt Hepler45af57b2020-10-23 08:05:28 -070067 if (defined(invoker.sources)) {
68 _all_py_files = invoker.sources
69 } else {
70 _all_py_files = []
71 }
72
73 if (defined(invoker.tests)) {
74 _test_sources = invoker.tests
75 } else {
76 _test_sources = []
77 }
78
Wyatt Hepler752d7d32021-03-02 09:02:23 -080079 # The Python targets are always instantiated in the default toolchain. Use
80 # fully qualified labels so that the toolchain is not lost.
81 _other_deps = []
82 if (defined(invoker.other_deps)) {
83 foreach(dep, invoker.other_deps) {
84 _other_deps += [ get_label_info(dep, "label_with_toolchain") ]
85 }
86 }
87
Wyatt Hepler45af57b2020-10-23 08:05:28 -070088 _all_py_files += _test_sources
89
Wyatt Hepler45af57b2020-10-23 08:05:28 -070090 # pw_python_script uses pw_python_package, but with a limited set of features.
91 # _pw_standalone signals that this target is actually a pw_python_script.
92 _is_package = !(defined(invoker._pw_standalone) && invoker._pw_standalone)
93
Alexei Frolova4c0aee2020-12-01 13:48:48 -080094 # Some build targets generate Python packages, setting _pw_generated to
95 # indicate this.
96 _is_generated_package =
97 defined(invoker._pw_generated) && invoker._pw_generated
98
Keir Mierlee65ddc82020-12-02 17:59:52 -080099 # Argument: invoker.lint = [true | false]; default = true.
100 # Default to false for generated packages, but allow overrides.
101 if (defined(invoker.lint)) {
102 _should_lint = invoker.lint
103 } else {
104 _should_lint = !_is_generated_package
105 }
106
Wyatt Hepler45af57b2020-10-23 08:05:28 -0700107 if (_is_package) {
108 assert(defined(invoker.setup) && invoker.setup != [],
109 "pw_python_package requires 'setup' to point to a setup.py file " +
110 "or pyproject.toml and setup.cfg files")
111
Alexei Frolova4c0aee2020-12-01 13:48:48 -0800112 if (!_is_generated_package) {
113 _all_py_files += invoker.setup
114 }
Wyatt Hepler45af57b2020-10-23 08:05:28 -0700115
116 # Get the directories of the setup files. All files must be in the same dir.
117 _setup_dirs = get_path_info(invoker.setup, "dir")
118 _setup_dir = _setup_dirs[0]
119
120 foreach(dir, _setup_dirs) {
121 assert(dir == _setup_dir,
122 "All files in 'setup' must be in the same directory")
123 }
124
125 # If sources are provided, make sure there is an __init__.py file.
Alexei Frolova4c0aee2020-12-01 13:48:48 -0800126 if (!_is_generated_package && defined(invoker.sources) &&
127 invoker.sources != []) {
Wyatt Hepler2e3619d2020-10-29 12:52:52 -0700128 assert(filter_include(invoker.sources, [ "*\b__init__.py" ]) != [],
Wyatt Hepler45af57b2020-10-23 08:05:28 -0700129 "Python packages must have at least one __init__.py file")
130 }
131 }
Wyatt Heplerb16dfd32020-10-12 08:46:38 -0700132
Wyatt Heplerd7dc6552020-10-21 16:16:21 -0700133 _python_deps = []
Wyatt Heplerb16dfd32020-10-12 08:46:38 -0700134 if (defined(invoker.python_deps)) {
Wyatt Heplerd7dc6552020-10-21 16:16:21 -0700135 foreach(dep, invoker.python_deps) {
136 # Use the fully qualified name so the subtarget can be appended as needed.
137 _python_deps += [ get_label_info(dep, "label_no_toolchain") ]
138 }
Wyatt Heplerb16dfd32020-10-12 08:46:38 -0700139 }
140
Wyatt Hepler620b3e02021-01-22 09:14:45 -0800141 # All dependencies needed for the package and its tests.
142 _python_test_deps = _python_deps
143 if (defined(invoker.python_test_deps)) {
144 foreach(test_dep, invoker.python_test_deps) {
145 _python_test_deps += [ get_label_info(test_dep, "label_no_toolchain") ]
146 }
147 }
148
149 if (_test_sources == []) {
150 assert(!defined(invoker.python_test_deps),
151 "python_test_deps was provided, but there are no tests in " +
152 get_label_info(":$target_name", "label_no_toolchain"))
153 not_needed(_python_test_deps)
154 }
155
Wyatt Hepler438caa02021-01-15 17:13:11 -0800156 _internal_target = "$target_name._internal"
157
158 # Create groups with the public target names ($target_name, $target_name.lint,
159 # $target_name.install, etc.). These are actually wrappers around internal
160 # Python actions instantiated with the default toolchain. This ensures there
161 # is only a single copy of each Python action in the build.
162 #
163 # The $target_name.tests group is created separately below.
164 foreach(subtarget, _python_subtargets - [ "tests" ]) {
165 group("$target_name.$subtarget") {
166 deps = [ ":$_internal_target.$subtarget($default_toolchain)" ]
167 }
168 }
169
170 group("$target_name") {
171 deps = [ ":$_internal_target($default_toolchain)" ]
172 }
173
Wyatt Heplerb16dfd32020-10-12 08:46:38 -0700174 # Declare the main Python package group. This represents the Python files, but
175 # does not take any actions. GN targets can depend on the package name to run
176 # when any files in the package change.
Wyatt Hepler438caa02021-01-15 17:13:11 -0800177 pw_input_group("$_internal_target") {
Wyatt Hepler45af57b2020-10-23 08:05:28 -0700178 inputs = _all_py_files
Wyatt Heplerb16dfd32020-10-12 08:46:38 -0700179 if (defined(invoker.inputs)) {
180 inputs += invoker.inputs
181 }
182
Wyatt Hepler752d7d32021-03-02 09:02:23 -0800183 deps = _python_deps + _other_deps
Wyatt Heplerb16dfd32020-10-12 08:46:38 -0700184 }
185
Wyatt Hepler45af57b2020-10-23 08:05:28 -0700186 if (_is_package) {
187 # Install this Python package and its dependencies in the current Python
188 # environment.
Wyatt Hepler438caa02021-01-15 17:13:11 -0800189 pw_python_action("$_internal_target.install") {
Wyatt Hepler45af57b2020-10-23 08:05:28 -0700190 module = "pip"
Wyatt Hepler285636f2020-12-15 13:13:11 -0800191 args = [ "install" ]
192
193 # Don't install generated packages with --editable, since the build
194 # directory is ephemeral.
195 if (!_is_generated_package) {
196 args += [ "--editable" ]
197 }
198 args += [ rebase_path(_setup_dir) ]
Wyatt Heplerb16dfd32020-10-12 08:46:38 -0700199
Wyatt Hepler45af57b2020-10-23 08:05:28 -0700200 stamp = true
Wyatt Heplerb16dfd32020-10-12 08:46:38 -0700201
Wyatt Hepler45af57b2020-10-23 08:05:28 -0700202 # Parallel pip installations don't work, so serialize pip invocations.
203 pool = "$dir_pw_build:pip_pool"
Wyatt Heplerd7dc6552020-10-21 16:16:21 -0700204
Wyatt Hepler438caa02021-01-15 17:13:11 -0800205 deps = [ ":$_internal_target" ]
Wyatt Hepler45af57b2020-10-23 08:05:28 -0700206 foreach(dep, _python_deps) {
Wyatt Hepler752d7d32021-03-02 09:02:23 -0800207 _subtarget = get_label_info(dep, "label_no_toolchain") + ".install"
208 deps += [ "$_subtarget(" + get_label_info(dep, "toolchain") + ")" ]
Wyatt Hepler45af57b2020-10-23 08:05:28 -0700209 }
Wyatt Heplerb16dfd32020-10-12 08:46:38 -0700210 }
Wyatt Heplerb16dfd32020-10-12 08:46:38 -0700211
Wyatt Hepler45af57b2020-10-23 08:05:28 -0700212 # TODO(pwbug/239): Add support for building groups of wheels. The code below
213 # is incomplete and untested.
Wyatt Hepler438caa02021-01-15 17:13:11 -0800214 pw_python_action("$_internal_target.wheel") {
Wyatt Hepler45af57b2020-10-23 08:05:28 -0700215 script = "$dir_pw_build/py/pw_build/python_wheels.py"
Wyatt Heplerb16dfd32020-10-12 08:46:38 -0700216
Wyatt Hepler45af57b2020-10-23 08:05:28 -0700217 args = [
218 "--out_dir",
219 rebase_path(target_out_dir),
220 ]
221 args += rebase_path(_all_py_files)
Wyatt Heplerb16dfd32020-10-12 08:46:38 -0700222
Wyatt Hepler438caa02021-01-15 17:13:11 -0800223 deps = [ ":$_internal_target.install" ]
Wyatt Hepler45af57b2020-10-23 08:05:28 -0700224 stamp = true
225 }
226 } else {
227 # If this is not a package, install or build wheels for its deps only.
Wyatt Hepler438caa02021-01-15 17:13:11 -0800228 group("$_internal_target.install") {
Wyatt Hepler45af57b2020-10-23 08:05:28 -0700229 deps = []
230 foreach(dep, _python_deps) {
231 deps += [ "$dep.install" ]
232 }
233 }
Wyatt Hepler438caa02021-01-15 17:13:11 -0800234 group("$_internal_target.wheel") {
Wyatt Hepler45af57b2020-10-23 08:05:28 -0700235 deps = []
236 foreach(dep, _python_deps) {
237 deps += [ "$dep.wheel" ]
238 }
239 }
Wyatt Heplerb16dfd32020-10-12 08:46:38 -0700240 }
241
242 # Define the static analysis targets for this package.
Wyatt Hepler438caa02021-01-15 17:13:11 -0800243 group("$_internal_target.lint") {
Wyatt Heplerb16dfd32020-10-12 08:46:38 -0700244 deps = [
Wyatt Hepler438caa02021-01-15 17:13:11 -0800245 ":$_internal_target.lint.mypy",
246 ":$_internal_target.lint.pylint",
Wyatt Heplerb16dfd32020-10-12 08:46:38 -0700247 ]
248 }
249
Wyatt Hepler620b3e02021-01-22 09:14:45 -0800250 if (_should_lint || _test_sources != []) {
251 # Packages that must be installed to use the package or run its tests.
Wyatt Hepler6554c142021-01-29 10:25:15 -0800252 _test_install_deps = [ ":$_internal_target.install" ]
Wyatt Heplera2970c52021-02-02 14:47:22 -0800253 foreach(dep, _python_test_deps + [ "$dir_pw_build:python_lint" ]) {
Wyatt Hepler620b3e02021-01-22 09:14:45 -0800254 _test_install_deps += [ "$dep.install" ]
255 }
256 }
257
Alexei Frolova4c0aee2020-12-01 13:48:48 -0800258 # For packages that are not generated, create targets to run mypy and pylint.
259 # Linting is not performed on generated packages.
Keir Mierlee65ddc82020-12-02 17:59:52 -0800260 if (_should_lint) {
Wyatt Hepler8ce90132020-12-03 10:57:20 -0800261 # Run lint tools from the setup or target directory so that the tools detect
262 # config files (e.g. pylintrc or mypy.ini) in that directory. Config files
263 # may be explicitly specified with the pylintrc or mypy_ini arguments.
264 if (defined(_setup_dir)) {
265 _lint_directory = rebase_path(_setup_dir)
266 } else {
267 _lint_directory = rebase_path(".")
268 }
269
Wyatt Hepler438caa02021-01-15 17:13:11 -0800270 pw_python_action("$_internal_target.lint.mypy") {
Alexei Frolova4c0aee2020-12-01 13:48:48 -0800271 module = "mypy"
272 args = [
273 "--pretty",
274 "--show-error-codes",
275 ]
Wyatt Hepler8ce90132020-12-03 10:57:20 -0800276
277 if (defined(invoker.mypy_ini)) {
278 args += [ "--config-file=" + rebase_path(invoker.mypy_ini) ]
279 inputs = [ invoker.mypy_ini ]
280 }
281
Wyatt Heplera2970c52021-02-02 14:47:22 -0800282 args += rebase_path(_all_py_files)
Alexei Frolova4c0aee2020-12-01 13:48:48 -0800283
284 # Use this environment variable to force mypy to colorize output.
285 # See https://github.com/python/mypy/issues/7771
286 environment = [ "MYPY_FORCE_COLOR=1" ]
287
Wyatt Hepler8ce90132020-12-03 10:57:20 -0800288 directory = _lint_directory
Alexei Frolova4c0aee2020-12-01 13:48:48 -0800289 stamp = true
290
Wyatt Hepler620b3e02021-01-22 09:14:45 -0800291 deps = _test_install_deps
292
293 foreach(dep, _python_test_deps) {
Alexei Frolova4c0aee2020-12-01 13:48:48 -0800294 deps += [ "$dep.lint.mypy" ]
295 }
Wyatt Hepler45af57b2020-10-23 08:05:28 -0700296 }
Wyatt Heplerb16dfd32020-10-12 08:46:38 -0700297
Alexei Frolova4c0aee2020-12-01 13:48:48 -0800298 # Create a target to run pylint on each of the Python files in this
299 # package and its dependencies.
Wyatt Hepler438caa02021-01-15 17:13:11 -0800300 pw_python_action_foreach("$_internal_target.lint.pylint") {
Alexei Frolova4c0aee2020-12-01 13:48:48 -0800301 module = "pylint"
302 args = [
Wyatt Hepler8ce90132020-12-03 10:57:20 -0800303 rebase_path(".") + "/{{source_target_relative}}",
Alexei Frolova4c0aee2020-12-01 13:48:48 -0800304 "--jobs=1",
305 "--output-format=colorized",
306 ]
Wyatt Heplerb16dfd32020-10-12 08:46:38 -0700307
Wyatt Hepler8ce90132020-12-03 10:57:20 -0800308 if (defined(invoker.pylintrc)) {
309 args += [ "--rcfile=" + rebase_path(invoker.pylintrc) ]
310 inputs = [ invoker.pylintrc ]
311 }
312
Alexei Frolova4c0aee2020-12-01 13:48:48 -0800313 if (host_os == "win") {
314 # Allow CRLF on Windows, in case Git is set to switch line endings.
315 args += [ "--disable=unexpected-line-ending-format" ]
316 }
Wyatt Heplerb16dfd32020-10-12 08:46:38 -0700317
Alexei Frolova4c0aee2020-12-01 13:48:48 -0800318 sources = _all_py_files
319
Wyatt Hepler8ce90132020-12-03 10:57:20 -0800320 directory = _lint_directory
321 stamp = "$target_gen_dir/{{source_target_relative}}.pylint.passed"
Alexei Frolova4c0aee2020-12-01 13:48:48 -0800322
Wyatt Hepler620b3e02021-01-22 09:14:45 -0800323 deps = _test_install_deps
324
325 foreach(dep, _python_test_deps) {
Alexei Frolova4c0aee2020-12-01 13:48:48 -0800326 deps += [ "$dep.lint.pylint" ]
327 }
Wyatt Heplerb16dfd32020-10-12 08:46:38 -0700328 }
Alexei Frolova4c0aee2020-12-01 13:48:48 -0800329 } else {
Wyatt Hepler438caa02021-01-15 17:13:11 -0800330 pw_input_group("$_internal_target.lint.mypy") {
Wyatt Hepler8ce90132020-12-03 10:57:20 -0800331 if (defined(invoker.pylintrc)) {
332 inputs = [ invoker.pylintrc ]
333 }
Wyatt Heplerb8e13602020-10-19 11:20:36 -0700334 }
Wyatt Hepler438caa02021-01-15 17:13:11 -0800335 pw_input_group("$_internal_target.lint.pylint") {
Wyatt Hepler8ce90132020-12-03 10:57:20 -0800336 if (defined(invoker.mypy_ini)) {
337 inputs = [ invoker.mypy_ini ]
338 }
Wyatt Heplerb16dfd32020-10-12 08:46:38 -0700339 }
340 }
341
342 # Create a target for each test file.
343 _test_targets = []
344
345 foreach(test, _test_sources) {
346 _test_name = string_replace(test, "/", "_")
Wyatt Hepler438caa02021-01-15 17:13:11 -0800347 _internal_test_target = "$_internal_target.tests.$_test_name"
Wyatt Heplerb16dfd32020-10-12 08:46:38 -0700348
Wyatt Hepler438caa02021-01-15 17:13:11 -0800349 pw_python_action(_internal_test_target) {
Wyatt Heplerb16dfd32020-10-12 08:46:38 -0700350 script = test
351 stamp = true
352
Wyatt Hepler620b3e02021-01-22 09:14:45 -0800353 deps = _test_install_deps
354
355 foreach(dep, _python_test_deps) {
Wyatt Heplerb16dfd32020-10-12 08:46:38 -0700356 deps += [ "$dep.tests" ]
357 }
358 }
359
Wyatt Hepler438caa02021-01-15 17:13:11 -0800360 # Create a public version of each test target, so tests can be executed as
361 # //path/to:package.tests.foo.py.
362 group("$target_name.tests.$_test_name") {
363 deps = [ ":$_internal_test_target" ]
364 }
365
366 _test_targets += [ ":$target_name.tests.$_test_name" ]
Wyatt Heplerb16dfd32020-10-12 08:46:38 -0700367 }
368
369 group("$target_name.tests") {
370 deps = _test_targets
371 }
372}
373
374# Declares a group of Python packages or other Python groups. pw_python_groups
375# expose the same set of subtargets as pw_python_package (e.g.
376# "$group_name.lint" and "$group_name.tests"), but these apply to all packages
377# in deps and their dependencies.
378template("pw_python_group") {
379 if (defined(invoker.python_deps)) {
380 _python_deps = invoker.python_deps
381 } else {
382 _python_deps = []
383 }
384
385 group(target_name) {
386 deps = _python_deps
387 }
388
Wyatt Hepler4d1e6aa2021-01-14 08:39:42 -0800389 foreach(subtarget, _python_subtargets) {
Wyatt Heplerb16dfd32020-10-12 08:46:38 -0700390 group("$target_name.$subtarget") {
391 deps = []
392 foreach(dep, _python_deps) {
393 # Split out the toolchain to support deps with a toolchain specified.
394 _target = get_label_info(dep, "label_no_toolchain")
395 _toolchain = get_label_info(dep, "toolchain")
396 deps += [ "$_target.$subtarget($_toolchain)" ]
397 }
398 }
399 }
400}
Wyatt Hepler45af57b2020-10-23 08:05:28 -0700401
402# Declares Python scripts or tests that are not part of a Python package.
403# Similar to pw_python_package, but only supports a subset of its features.
404#
405# pw_python_script accepts the same arguments as pw_python_package, except
406# `setup` cannot be provided.
407#
408# pw_python_script provides the same subtargets as pw_python_package, but
409# $target_name.install and $target_name.wheel only affect the python_deps of
410# this GN target, not the target itself.
411template("pw_python_script") {
412 _supported_variables = [
413 "sources",
414 "tests",
415 "python_deps",
416 "other_deps",
417 "inputs",
Wyatt Hepler8ce90132020-12-03 10:57:20 -0800418 "pylintrc",
Wyatt Hepler45af57b2020-10-23 08:05:28 -0700419 ]
420
421 pw_python_package(target_name) {
422 _pw_standalone = true
423 forward_variables_from(invoker, _supported_variables)
424 }
425}
Wyatt Hepler4d1e6aa2021-01-14 08:39:42 -0800426
427# Represents a list of Python requirements, as in a requirements.txt.
428#
429# Args:
430# files: One or more requirements.txt files.
431# requirements: A list of requirements.txt-style requirements.
432template("pw_python_requirements") {
433 assert(defined(invoker.files) || defined(invoker.requirements),
434 "pw_python_requirements requires a list of requirements.txt files " +
435 "in the 'files' arg or requirements in 'requirements'")
436
437 _requirements_files = []
438
439 if (defined(invoker.files)) {
440 _requirements_files += invoker.files
441 }
442
443 if (defined(invoker.requirements)) {
444 _requirements_file = "$target_gen_dir/$target_name.requirements.txt"
445 write_file(_requirements_file, invoker.requirements)
446 _requirements_files += [ _requirements_file ]
447 }
448
449 # The default target represents the requirements themselves.
450 pw_input_group(target_name) {
451 inputs = _requirements_files
452 }
453
454 # Use the same subtargets as pw_python_package so these targets can be listed
455 # as python_deps of pw_python_packages.
456 pw_python_action("$target_name.install") {
457 inputs = _requirements_files
458
459 module = "pip"
460 args = [ "install" ]
461
462 foreach(_requirements_file, inputs) {
463 args += [
464 "--requirement",
465 rebase_path(_requirements_file),
466 ]
467 }
468
469 pool = "$dir_pw_build:pip_pool"
470 stamp = true
471 }
472
473 # Create stubs for the unused subtargets so that pw_python_requirements can be
474 # used as python_deps.
475 foreach(subtarget, _python_subtargets - [ "install" ]) {
476 group("$target_name.$subtarget") {
477 }
478 }
479}