blob: bc09d5a249d322f8470ec3ad772c580368089045 [file] [log] [blame]
Michael Thvedt828a4be2015-08-12 17:45:36 +00001#!/usr/bin/python2.7
2
Damien Martin-Guillerezf88f4d82015-09-25 13:56:55 +00003# Copyright 2015 The Bazel Authors. All rights reserved.
Michael Thvedt828a4be2015-08-12 17:45:36 +00004#
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"""A script for J2ObjC dead code removal in Blaze.
18
19This script removes unused J2ObjC-translated classes from compilation and
20linking by:
21 1. Build a class dependency tree among translated source files.
22 2. Use user-provided Java class entry points to get a list of reachable
23 classes.
24 3. Go through all translated source files and rewrite unreachable ones with
25 dummy content.
26"""
27
28import argparse
29from collections import OrderedDict
30import multiprocessing
31import os
diamondm2732b172018-05-24 12:58:37 -070032import pipes # swap to shlex once on Python 3
Chris Parsons87846c72016-05-05 20:24:13 +000033import re
Michael Thvedt828a4be2015-08-12 17:45:36 +000034import shutil
Rumou Duan123e1c32016-02-01 16:16:15 +000035import subprocess
Michael Thvedt828a4be2015-08-12 17:45:36 +000036import threading
37
Googler05883a82020-10-12 09:27:09 -070038from six.moves import queue as Queue # pylint: disable=redefined-builtin
39from six.moves import xrange # pylint: disable=redefined-builtin
40
Michael Thvedt828a4be2015-08-12 17:45:36 +000041PRUNED_SRC_CONTENT = 'static int DUMMY_unused __attribute__((unused,used)) = 0;'
42
43
44def BuildReachabilityTree(dependency_mapping_files, file_open=open):
45 """Builds a reachability tree using entries from dependency mapping files.
46
47 Args:
48 dependency_mapping_files: A comma separated list of J2ObjC-generated
49 dependency mapping files.
50 file_open: Reference to the builtin open function so it may be
51 overridden for testing.
52 Returns:
53 A dict mapping J2ObjC-generated source files to the corresponding direct
54 dependent source files.
55 """
Rgis Dcamps7ae5adb2019-10-07 00:49:56 -070056 return BuildArtifactSourceTree(dependency_mapping_files, file_open)
Michael Thvedt828a4be2015-08-12 17:45:36 +000057
58
59def BuildHeaderMapping(header_mapping_files, file_open=open):
60 """Builds a mapping between Java classes and J2ObjC-translated header files.
61
62 Args:
63 header_mapping_files: A comma separated list of J2ObjC-generated
64 header mapping files.
65 file_open: Reference to the builtin open function so it may be
66 overridden for testing.
67 Returns:
68 An ordered dict mapping Java class names to corresponding J2ObjC-translated
69 source files.
70 """
71 header_mapping = OrderedDict()
72 for header_mapping_file in header_mapping_files.split(','):
73 with file_open(header_mapping_file, 'r') as f:
74 for line in f:
75 java_class_name = line.strip().split('=')[0]
76 transpiled_file_name = os.path.splitext(line.strip().split('=')[1])[0]
77 header_mapping[java_class_name] = transpiled_file_name
78 return header_mapping
79
80
Rumou Duan123e1c32016-02-01 16:16:15 +000081def BuildReachableFileSet(entry_classes, reachability_tree, header_mapping,
82 archive_source_file_mapping=None):
Michael Thvedt828a4be2015-08-12 17:45:36 +000083 """Builds a set of reachable translated files from entry Java classes.
84
85 Args:
86 entry_classes: A comma separated list of Java entry classes.
87 reachability_tree: A dict mapping translated files to their direct
88 dependencies.
89 header_mapping: A dict mapping Java class names to translated source files.
Rumou Duan123e1c32016-02-01 16:16:15 +000090 archive_source_file_mapping: A dict mapping source files to the associated
91 archive file that contains them.
Michael Thvedt828a4be2015-08-12 17:45:36 +000092 Returns:
93 A set of reachable translated files from the given list of entry classes.
94 Raises:
95 Exception: If there is an entry class that is not being transpiled in this
96 j2objc_library.
97 """
Rumou Duan44a7a6c2015-09-16 22:28:32 +000098 transpiled_entry_files = []
Michael Thvedt828a4be2015-08-12 17:45:36 +000099 for entry_class in entry_classes.split(','):
100 if entry_class not in header_mapping:
101 raise Exception(entry_class +
Googlerea481732017-08-07 18:02:11 +0200102 ' is not in the transitive Java deps of included ' +
Michael Thvedt828a4be2015-08-12 17:45:36 +0000103 'j2objc_library rules.')
Rumou Duan44a7a6c2015-09-16 22:28:32 +0000104 transpiled_entry_files.append(header_mapping[entry_class])
105
Rumou Duan123e1c32016-02-01 16:16:15 +0000106 # Translated files going into the same static library archive with duplicated
107 # base names also need to be added to the set of entry files.
108 #
109 # This edge case is ignored because we currently cannot correctly perform
110 # dead code removal in this case. The object file entries in static library
111 # archives are named by the base names of the original source files. If two
112 # source files (e.g., foo/bar.m, bar/bar.m) go into the same archive and
113 # share the same base name (bar.m), their object file entries inside the
114 # archive will have the same name (bar.o). We cannot correctly handle this
115 # case because current archive tools (ar, ranlib, etc.) do not handle this
116 # case very well.
117 if archive_source_file_mapping:
118 transpiled_entry_files.extend(_DuplicatedFiles(archive_source_file_mapping))
119
Rumou Duan44a7a6c2015-09-16 22:28:32 +0000120 # Translated files from package-info.java are also added to the entry files
121 # because they are needed to resolve ObjC class names with prefixes and these
122 # files may also have dependencies.
123 for transpiled_file in reachability_tree:
124 if transpiled_file.endswith('package-info'):
125 transpiled_entry_files.append(transpiled_file)
126
127 reachable_files = set()
128 for transpiled_entry_file in transpiled_entry_files:
129 reachable_files.add(transpiled_entry_file)
Miguel Alcon Pinto933c13a2015-09-16 18:37:45 +0000130 current_level_deps = []
131 # We need to check if the transpiled file is in the reachability tree
132 # because J2ObjC protos are not analyzed for dead code stripping and
133 # therefore are not in the reachability tree at all.
Rumou Duan44a7a6c2015-09-16 22:28:32 +0000134 if transpiled_entry_file in reachability_tree:
135 current_level_deps = reachability_tree[transpiled_entry_file]
Michael Thvedt828a4be2015-08-12 17:45:36 +0000136 while current_level_deps:
137 next_level_deps = []
138 for dep in current_level_deps:
139 if dep not in reachable_files:
140 reachable_files.add(dep)
141 if dep in reachability_tree:
142 next_level_deps.extend(reachability_tree[dep])
143 current_level_deps = next_level_deps
144 return reachable_files
145
146
147def PruneFiles(input_files, output_files, objc_file_path, reachable_files,
148 file_open=open, file_shutil=shutil):
149 """Copies over translated files and remove the contents of unreachable files.
150
151 Args:
152 input_files: A comma separated list of input source files to prune. It has
153 a one-on-one pair mapping with the output_file list.
154 output_files: A comma separated list of output source files to write pruned
155 source files to. It has a one-on-one pair mapping with the input_file
156 list.
157 objc_file_path: The file path which represents a directory where the
158 generated ObjC files reside.
159 reachable_files: A set of reachable source files.
160 file_open: Reference to the builtin open function so it may be
161 overridden for testing.
162 file_shutil: Reference to the builtin shutil module so it may be
163 overridden for testing.
164 Returns:
165 None.
166 """
167 file_queue = Queue.Queue()
168 for input_file, output_file in zip(input_files.split(','),
169 output_files.split(',')):
170 file_queue.put((input_file, output_file))
171
172 for _ in xrange(multiprocessing.cpu_count()):
173 t = threading.Thread(target=_PruneFile, args=(file_queue,
174 reachable_files,
175 objc_file_path,
176 file_open,
177 file_shutil))
178 t.start()
179
180 file_queue.join()
181
182
183def _PruneFile(file_queue, reachable_files, objc_file_path, file_open=open,
184 file_shutil=shutil):
185 while True:
186 try:
187 input_file, output_file = file_queue.get_nowait()
188 except Queue.Empty:
189 return
190 file_name = os.path.relpath(os.path.splitext(input_file)[0],
191 objc_file_path)
Rumou Duan44a7a6c2015-09-16 22:28:32 +0000192 if file_name in reachable_files:
Michael Thvedt828a4be2015-08-12 17:45:36 +0000193 file_shutil.copy(input_file, output_file)
194 else:
laszlocsomorf11c6bc2018-07-05 01:58:06 -0700195 with file_open(output_file, 'w') as f:
jingwen4a74c522018-11-20 11:57:15 -0800196 # Use a static variable scoped to the source file to suppress
laszlocsomorf11c6bc2018-07-05 01:58:06 -0700197 # the "has no symbols" linker warning for empty object files.
198 f.write(PRUNED_SRC_CONTENT)
Michael Thvedt828a4be2015-08-12 17:45:36 +0000199 file_queue.task_done()
200
201
Rumou Duan123e1c32016-02-01 16:16:15 +0000202def _DuplicatedFiles(archive_source_file_mapping):
203 """Returns a list of file with duplicated base names in each archive file.
204
205 Args:
206 archive_source_file_mapping: A dict mapping source files to the associated
207 archive file that contains them.
208 Returns:
jingwen4a74c522018-11-20 11:57:15 -0800209 A list containing files with duplicated base names.
Rumou Duan123e1c32016-02-01 16:16:15 +0000210 """
211 duplicated_files = []
212 dict_with_duplicates = dict()
213
Googler09894d12017-12-19 13:32:24 -0800214 for source_files in archive_source_file_mapping.values():
Rumou Duan123e1c32016-02-01 16:16:15 +0000215 for source_file in source_files:
216 file_basename = os.path.basename(source_file)
217 file_without_ext = os.path.splitext(source_file)[0]
218 if file_basename in dict_with_duplicates:
219 dict_with_duplicates[file_basename].append(file_without_ext)
220 else:
221 dict_with_duplicates[file_basename] = [file_without_ext]
222 for basename in dict_with_duplicates:
223 if len(dict_with_duplicates[basename]) > 1:
224 duplicated_files.extend(dict_with_duplicates[basename])
225 dict_with_duplicates = dict()
226
227 return duplicated_files
228
229
230def BuildArchiveSourceFileMapping(archive_source_mapping_files, file_open):
231 """Builds a mapping between archive files and their associated source files.
232
233 Args:
234 archive_source_mapping_files: A comma separated list of J2ObjC-generated
235 mapping between archive files and their associated source files.
236 file_open: Reference to the builtin open function so it may be
237 overridden for testing.
238 Returns:
239 A dict mapping between archive files and their associated source files.
240 """
Rgis Dcamps7ae5adb2019-10-07 00:49:56 -0700241 return BuildArtifactSourceTree(archive_source_mapping_files, file_open)
Rumou Duan123e1c32016-02-01 16:16:15 +0000242
243
244def PruneSourceFiles(input_files, output_files, dependency_mapping_files,
245 header_mapping_files, entry_classes, objc_file_path,
246 file_open=open, file_shutil=shutil):
Michael Thvedt828a4be2015-08-12 17:45:36 +0000247 """Copies over translated files and remove the contents of unreachable files.
248
249 Args:
250 input_files: A comma separated list of input source files to prune. It has
251 a one-on-one pair mapping with the output_file list.
252 output_files: A comma separated list of output source files to write pruned
253 source files to. It has a one-on-one pair mapping with the input_file
254 list.
255 dependency_mapping_files: A comma separated list of J2ObjC-generated
256 dependency mapping files.
257 header_mapping_files: A comma separated list of J2ObjC-generated
258 header mapping files.
259 entry_classes: A comma separated list of Java entry classes.
260 objc_file_path: The file path which represents a directory where the
261 generated ObjC files reside.
262 file_open: Reference to the builtin open function so it may be
263 overridden for testing.
264 file_shutil: Reference to the builtin shutil module so it may be
265 overridden for testing.
Michael Thvedt828a4be2015-08-12 17:45:36 +0000266 """
267 reachability_file_mapping = BuildReachabilityTree(
268 dependency_mapping_files, file_open)
269 header_map = BuildHeaderMapping(header_mapping_files, file_open)
270 reachable_files_set = BuildReachableFileSet(entry_classes,
271 reachability_file_mapping,
272 header_map)
273 PruneFiles(input_files,
274 output_files,
275 objc_file_path,
276 reachable_files_set,
277 file_open,
278 file_shutil)
279
280
Chris Parsons87846c72016-05-05 20:24:13 +0000281def MatchObjectNamesInArchive(xcrunwrapper, archive, object_names):
282 """Returns object names matching their identity in an archive file.
283
284 The linker that blaze uses appends an md5 hash to object file
285 names prior to inclusion in the archive file. Thus, object names
286 such as 'foo.o' need to be matched to their appropriate name in
287 the archive file, such as 'foo_<hash>.o'.
288
289 Args:
290 xcrunwrapper: A wrapper script over xcrun.
291 archive: The location of the archive file.
292 object_names: The expected basenames of object files to match,
293 sans extension. For example 'foo' (not 'foo.o').
294 Returns:
295 A list of basenames of matching members of the given archive
296 """
diamondm2732b172018-05-24 12:58:37 -0700297 ar_contents_cmd = [xcrunwrapper, 'ar', '-t', archive]
298 real_object_names = subprocess.check_output(ar_contents_cmd)
Googler62af2b42018-03-22 13:22:44 -0700299 expected_object_name_regex = r'^(?:%s)(?:_[0-9a-f]{32}(?:-[0-9]+)?)?\.o$' % (
Chris Parsons87846c72016-05-05 20:24:13 +0000300 '|'.join([re.escape(name) for name in object_names]))
301 return re.findall(expected_object_name_regex, real_object_names,
302 flags=re.MULTILINE)
303
304
Rumou Duan123e1c32016-02-01 16:16:15 +0000305def PruneArchiveFile(input_archive, output_archive, dummy_archive,
306 dependency_mapping_files, header_mapping_files,
307 archive_source_mapping_files, entry_classes, xcrunwrapper,
Chris Parsons87846c72016-05-05 20:24:13 +0000308 file_open=open):
Rumou Duan123e1c32016-02-01 16:16:15 +0000309 """Remove unreachable objects from archive file.
310
311 Args:
312 input_archive: The source archive file to prune.
313 output_archive: The location of the pruned archive file.
314 dummy_archive: A dummy archive file that contains no object.
315 dependency_mapping_files: A comma separated list of J2ObjC-generated
316 dependency mapping files.
317 header_mapping_files: A comma separated list of J2ObjC-generated
318 header mapping files.
319 archive_source_mapping_files: A comma separated list of J2ObjC-generated
320 mapping between archive files and their associated source files.
321 entry_classes: A comma separated list of Java entry classes.
322 xcrunwrapper: A wrapper script over xcrun.
323 file_open: Reference to the builtin open function so it may be
324 overridden for testing.
Rumou Duan123e1c32016-02-01 16:16:15 +0000325 """
326 reachability_file_mapping = BuildReachabilityTree(
327 dependency_mapping_files, file_open)
328 header_map = BuildHeaderMapping(header_mapping_files, file_open)
329 archive_source_file_mapping = BuildArchiveSourceFileMapping(
330 archive_source_mapping_files, file_open)
331 reachable_files_set = BuildReachableFileSet(entry_classes,
332 reachability_file_mapping,
333 header_map,
334 archive_source_file_mapping)
335
kaipi06b49282018-06-26 12:25:58 -0700336 # Copy the current processes' environment, as xcrunwrapper depends on these
337 # variables.
338 cmd_env = dict(os.environ)
Rumou Duan123e1c32016-02-01 16:16:15 +0000339 j2objc_cmd = ''
340 if input_archive in archive_source_file_mapping:
341 source_files = archive_source_file_mapping[input_archive]
342 unreachable_object_names = []
343
344 for source_file in source_files:
345 if os.path.splitext(source_file)[0] not in reachable_files_set:
346 unreachable_object_names.append(
Chris Parsons87846c72016-05-05 20:24:13 +0000347 os.path.basename(os.path.splitext(source_file)[0]))
Rumou Duan123e1c32016-02-01 16:16:15 +0000348
349 # There are unreachable objects in the archive to prune
350 if unreachable_object_names:
351 # If all objects in the archive are unreachable, just copy over a dummy
352 # archive that contains no object
353 if len(unreachable_object_names) == len(source_files):
diamondm2732b172018-05-24 12:58:37 -0700354 j2objc_cmd = 'cp %s %s' % (pipes.quote(dummy_archive),
355 pipes.quote(output_archive))
Rumou Duan123e1c32016-02-01 16:16:15 +0000356 # Else we need to prune the archive of unreachable objects
357 else:
Googler60a7e632016-08-31 19:44:21 +0000358 cmd_env['ZERO_AR_DATE'] = '1'
Rumou Duan123e1c32016-02-01 16:16:15 +0000359 # Copy the input archive to the output location
diamondm2732b172018-05-24 12:58:37 -0700360 j2objc_cmd += 'cp %s %s && ' % (pipes.quote(input_archive),
361 pipes.quote(output_archive))
Rumou Duan123e1c32016-02-01 16:16:15 +0000362 # Make the output archive editable
diamondm2732b172018-05-24 12:58:37 -0700363 j2objc_cmd += 'chmod +w %s && ' % (pipes.quote(output_archive))
Rumou Duan123e1c32016-02-01 16:16:15 +0000364 # Remove the unreachable objects from the archive
Chris Parsons87846c72016-05-05 20:24:13 +0000365 unreachable_object_names = MatchObjectNamesInArchive(
366 xcrunwrapper, input_archive, unreachable_object_names)
Rumou Duanf95bfa62016-11-10 18:52:49 +0000367 j2objc_cmd += '%s ar -d -s %s %s && ' % (
diamondm2732b172018-05-24 12:58:37 -0700368 pipes.quote(xcrunwrapper),
369 pipes.quote(output_archive),
370 ' '.join(pipes.quote(uon) for uon in unreachable_object_names))
Rumou Duan123e1c32016-02-01 16:16:15 +0000371 # Update the table of content of the archive file
diamondm2732b172018-05-24 12:58:37 -0700372 j2objc_cmd += '%s ranlib %s' % (pipes.quote(xcrunwrapper),
373 pipes.quote(output_archive))
Rumou Duan123e1c32016-02-01 16:16:15 +0000374 # There are no unreachable objects, we just copy over the original archive
375 else:
diamondm2732b172018-05-24 12:58:37 -0700376 j2objc_cmd = 'cp %s %s' % (pipes.quote(input_archive),
377 pipes.quote(output_archive))
Rumou Duan123e1c32016-02-01 16:16:15 +0000378 # The archive cannot be pruned by J2ObjC dead code removal, just copy over
379 # the original archive
380 else:
diamondm2732b172018-05-24 12:58:37 -0700381 j2objc_cmd = 'cp %s %s' % (pipes.quote(input_archive),
382 pipes.quote(output_archive))
Rumou Duan123e1c32016-02-01 16:16:15 +0000383
Googlerfc60b3c2019-02-04 07:35:41 -0800384 try:
385 subprocess.check_output(
386 j2objc_cmd, stderr=subprocess.STDOUT, shell=True, env=cmd_env)
387 except OSError as e:
388 raise Exception(
389 'executing command failed: %s (%s)' % (j2objc_cmd, e.strerror))
Googler60a7e632016-08-31 19:44:21 +0000390
391 # "Touch" the output file.
392 # Prevents a pre-Xcode-8 bug in which passing zero-date archive files to ld
393 # would cause ld to error.
Rumou Duanf95bfa62016-11-10 18:52:49 +0000394 os.utime(output_archive, None)
Rumou Duan123e1c32016-02-01 16:16:15 +0000395
396
Rgis Dcamps7ae5adb2019-10-07 00:49:56 -0700397def BuildArtifactSourceTree(files, file_open=open):
398 """Builds a dependency tree using from dependency mapping files.
399
400 Args:
401 files: A comma separated list of dependency mapping files.
402 file_open: Reference to the builtin open function so it may be overridden for
403 testing.
404
405 Returns:
406 A dict mapping build artifacts (possibly generated source files) to the
407 corresponding direct dependent source files.
408 """
409 tree = dict()
Googler07eb53d2019-10-21 00:43:59 -0700410 if not files:
411 return tree
Rgis Dcamps7ae5adb2019-10-07 00:49:56 -0700412 for filename in files.split(','):
413 with file_open(filename, 'r') as f:
414 for line in f:
415 entry = line.strip().split(':')[0]
416 dep = line.strip().split(':')[1]
417 if entry in tree:
418 tree[entry].append(dep)
419 else:
420 tree[entry] = [dep]
421 return tree
422
423
Michael Thvedt828a4be2015-08-12 17:45:36 +0000424if __name__ == '__main__':
Rumou Duanab16dd62015-08-18 21:52:08 +0000425 parser = argparse.ArgumentParser(fromfile_prefix_chars='@')
Rumou Duan123e1c32016-02-01 16:16:15 +0000426
427 # TODO(rduan): Remove these three flags once J2ObjC compile actions are fully
428 # moved to the edges.
Michael Thvedt828a4be2015-08-12 17:45:36 +0000429 parser.add_argument(
430 '--input_files',
Michael Thvedt828a4be2015-08-12 17:45:36 +0000431 help=('The comma-separated file paths of translated source files to '
432 'prune.'))
433 parser.add_argument(
434 '--output_files',
Michael Thvedt828a4be2015-08-12 17:45:36 +0000435 help='The comma-separated file paths of pruned source files to write to.')
436 parser.add_argument(
Rumou Duan123e1c32016-02-01 16:16:15 +0000437 '--objc_file_path',
438 help='The file path which represents a directory where the generated ObjC'
439 ' files reside')
440
441 parser.add_argument(
442 '--input_archive',
443 help=('The path of the translated archive to prune.'))
444 parser.add_argument(
445 '--output_archive',
446 help='The path of the pruned archive file to write to.')
447 parser.add_argument(
448 '--dummy_archive',
449 help='The dummy archive file that contains no symbol.')
450 parser.add_argument(
Michael Thvedt828a4be2015-08-12 17:45:36 +0000451 '--dependency_mapping_files',
Michael Thvedt828a4be2015-08-12 17:45:36 +0000452 help='The comma-separated file paths of dependency mapping files.')
453 parser.add_argument(
454 '--header_mapping_files',
Michael Thvedt828a4be2015-08-12 17:45:36 +0000455 help='The comma-separated file paths of header mapping files.')
456 parser.add_argument(
Rumou Duan123e1c32016-02-01 16:16:15 +0000457 '--archive_source_mapping_files',
458 help='The comma-separated file paths of archive to source mapping files.'
459 'These mapping files should contain mappings between the '
460 'translated source files and the archive file compiled from those '
461 'source files.')
462 parser.add_argument(
Michael Thvedt828a4be2015-08-12 17:45:36 +0000463 '--entry_classes',
Michael Thvedt828a4be2015-08-12 17:45:36 +0000464 help=('The comma-separated list of Java entry classes to be used as entry'
465 ' point of the dead code anlysis.'))
466 parser.add_argument(
Rumou Duan123e1c32016-02-01 16:16:15 +0000467 '--xcrunwrapper',
468 help=('The xcrun wrapper script.'))
469
Michael Thvedt828a4be2015-08-12 17:45:36 +0000470 args = parser.parse_args()
471
472 if not args.entry_classes:
473 raise Exception('J2objC dead code removal is on but no entry class is ',
474 'specified in any j2objc_library targets in the transitive',
475 ' closure')
Rumou Duan123e1c32016-02-01 16:16:15 +0000476 if args.input_archive and args.output_archive:
477 PruneArchiveFile(
478 args.input_archive,
479 args.output_archive,
480 args.dummy_archive,
481 args.dependency_mapping_files,
482 args.header_mapping_files,
483 args.archive_source_mapping_files,
484 args.entry_classes,
485 args.xcrunwrapper)
486 else:
487 # TODO(rduan): Remove once J2ObjC compile actions are fully moved to the
488 # edges.
489 PruneSourceFiles(
490 args.input_files,
491 args.output_files,
492 args.dependency_mapping_files,
493 args.header_mapping_files,
494 args.entry_classes,
495 args.objc_file_path)