blob: e6f4c75dbd5c12ae0ce8994aeffbf5c157c1c43e [file] [log] [blame]
Googler041f7ce2022-03-23 21:32:08 -07001#!/usr/bin/python3
2
3# Copyright 2015 The Bazel Authors. All rights reserved.
4#
5# Licensed under the Apache License, Version 2.0 (the "License");
6# you may not use this file except in compliance with the License.
7# You may obtain a copy of the License at
8#
9# http://www.apache.org/licenses/LICENSE-2.0
10#
11# Unless required by applicable law or agreed to in writing, software
12# distributed under the License is distributed on an "AS IS" BASIS,
13# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14# See the License for the specific language governing permissions and
15# limitations under the License.
16
17"""A wrapper script for J2ObjC transpiler.
18
19This script wraps around J2ObjC transpiler to also output a dependency mapping
20file by scanning the import and include directives of the J2ObjC-translated
21files.
22"""
23
24import argparse
25import errno
26import multiprocessing
27import os
28import queue
29import re
30import shutil
31import subprocess
32import tempfile
33import threading
34import zipfile
35
36_INCLUDE_RE = re.compile('#(include|import) "([^"]+)"')
37_CONST_DATE_TIME = [1980, 1, 1, 0, 0, 0]
38_ADD_EXPORTS = [
39 '--add-exports=jdk.compiler/com.sun.tools.javac.parser=ALL-UNNAMED',
40 '--add-exports=jdk.compiler/com.sun.tools.javac.tree=ALL-UNNAMED',
41 '--add-exports=jdk.compiler/com.sun.tools.javac.util=ALL-UNNAMED',
42 '--add-exports=jdk.compiler/com.sun.tools.javac.code=ALL-UNNAMED',
43 '--add-exports=jdk.compiler/com.sun.tools.javac.api=ALL-UNNAMED',
44]
45
46
47def RunJ2ObjC(java, jvm_flags, j2objc, main_class, output_file_path,
48 j2objc_args, source_paths, files_to_translate):
49 """Runs J2ObjC transpiler to translate Java source files to ObjC.
50
51 Args:
52 java: The path of the Java executable.
53 jvm_flags: A comma-separated list of flags to pass to JVM.
54 j2objc: The deploy jar of J2ObjC.
55 main_class: The J2ObjC main class to invoke.
56 output_file_path: The output file directory.
57 j2objc_args: A list of args to pass to J2ObjC transpiler.
58 source_paths: A list of directories that contain sources to translate.
59 files_to_translate: A list of relative paths (relative to source_paths) that
60 point to sources to translate.
61 Returns:
62 None.
63 """
64 j2objc_args.extend(['-sourcepath', ':'.join(source_paths)])
65 j2objc_args.extend(['-d', output_file_path])
66 j2objc_args.extend(files_to_translate)
67 param_file_content = ' '.join(j2objc_args).encode('utf-8')
68 fd = None
69 param_filename = None
70 try:
71 fd, param_filename = tempfile.mkstemp(text=True)
72 os.write(fd, param_file_content)
73 finally:
74 if fd:
75 os.close(fd)
76 try:
77 j2objc_cmd = [java]
78 j2objc_cmd.extend([f_ for f_ in jvm_flags.split(',') if f_])
79 j2objc_cmd.extend(_ADD_EXPORTS)
80 j2objc_cmd.extend(['-cp', j2objc, main_class])
81 j2objc_cmd.append('@%s' % param_filename)
82 subprocess.check_call(j2objc_cmd, stderr=subprocess.STDOUT)
83 finally:
84 if param_filename:
85 os.remove(param_filename)
86
87
88def WriteDepMappingFile(objc_files,
89 objc_file_root,
90 output_dependency_mapping_file,
91 file_open=open):
92 """Scans J2ObjC-translated files and outputs a dependency mapping file.
93
94 The mapping file contains mappings between translated source files and their
95 imported source files scanned from the import and include directives.
96
97 Args:
98 objc_files: A list of ObjC files translated by J2ObjC.
99 objc_file_root: The file path which represents a directory where the
100 generated ObjC files reside.
101 output_dependency_mapping_file: The path of the dependency mapping file to
102 write to.
103 file_open: Reference to the builtin open function so it may be
104 overridden for testing.
105 Raises:
106 RuntimeError: If spawned threads throw errors during processing.
107 Returns:
108 None.
109 """
110 dep_mapping = dict()
111 input_file_queue = queue.Queue()
112 output_dep_mapping_queue = queue.Queue()
113 error_message_queue = queue.Queue()
114 for objc_file in objc_files:
115 input_file_queue.put(os.path.join(objc_file_root, objc_file))
116
117 for _ in range(multiprocessing.cpu_count()):
118 t = threading.Thread(target=_ReadDepMapping, args=(input_file_queue,
119 output_dep_mapping_queue,
120 error_message_queue,
121 objc_file_root,
122 file_open))
123 t.start()
124
125 input_file_queue.join()
126
127 if not error_message_queue.empty():
128 error_messages = list(error_message_queue.queue)
129 raise RuntimeError('\n'.join(error_messages))
130
131 while not output_dep_mapping_queue.empty():
132 entry_file, deps = output_dep_mapping_queue.get()
133 dep_mapping[entry_file] = deps
134
135 with file_open(output_dependency_mapping_file, 'w') as f:
136 for entry in sorted(dep_mapping):
137 for dep in dep_mapping[entry]:
138 f.write(entry + ':' + dep + '\n')
139
140
141def _ReadDepMapping(input_file_queue, output_dep_mapping_queue,
142 error_message_queue, output_root, file_open=open):
143 while True:
144 try:
145 input_file = input_file_queue.get_nowait()
146 except queue.Empty:
147 # No more work left in the queue.
148 return
149
150 try:
151 deps = set()
152 input_file_name = os.path.splitext(input_file)[0]
153 entry = os.path.relpath(input_file_name, output_root)
154 for file_ext in ['.m', '.h']:
155 with file_open(input_file_name + file_ext, 'r') as f:
156 for line in f:
157 include = _INCLUDE_RE.match(line)
158 if include:
159 include_path = include.group(2)
160 dep = os.path.splitext(include_path)[0]
161 if dep != entry:
162 deps.add(dep)
163
164 output_dep_mapping_queue.put((entry, sorted(deps)))
165 except Exception as e: # pylint: disable=broad-except
166 error_message_queue.put(str(e))
167 finally:
168 # We need to mark the task done to prevent blocking the main process
169 # indefinitely.
170 input_file_queue.task_done()
171
172
173def WriteArchiveSourceMappingFile(compiled_archive_file_path,
174 output_archive_source_mapping_file,
175 objc_files,
176 file_open=open):
177 """Writes a mapping file between archive file to associated ObjC source files.
178
179 Args:
180 compiled_archive_file_path: The path of the archive file.
181 output_archive_source_mapping_file: A path of the mapping file to write to.
182 objc_files: A list of ObjC files translated by J2ObjC.
183 file_open: Reference to the builtin open function so it may be
184 overridden for testing.
185 Returns:
186 None.
187 """
188 with file_open(output_archive_source_mapping_file, 'w') as f:
189 for objc_file in objc_files:
190 f.write(compiled_archive_file_path + ':' + objc_file + '\n')
191
192
193def _ParseArgs(j2objc_args):
194 """Separate arguments passed to J2ObjC into source files and J2ObjC flags.
195
196 Args:
197 j2objc_args: A list of args to pass to J2ObjC transpiler.
198 Returns:
199 A tuple containing source files and J2ObjC flags
200 """
201 source_files = []
202 flags = []
203 is_next_flag_value = False
204 for j2objc_arg in j2objc_args:
205 if j2objc_arg.startswith('-'):
206 flags.append(j2objc_arg)
207 is_next_flag_value = True
208 elif is_next_flag_value:
209 flags.append(j2objc_arg)
210 is_next_flag_value = False
211 else:
212 source_files.append(j2objc_arg)
213 return (source_files, flags)
214
215
216def _J2ObjcOutputObjcFiles(java_files):
217 """Returns the relative paths of the associated output ObjC source files.
218
219 Args:
220 java_files: The list of Java files to translate.
221 Returns:
222 A list of associated output ObjC source files.
223 """
224 return [os.path.splitext(java_file)[0] + '.m' for java_file in java_files]
225
226
227def UnzipSourceJarSources(source_jars):
228 """Unzips the source jars containing Java source files.
229
230 Args:
231 source_jars: The list of input Java source jars.
232 Returns:
233 A tuple of the temporary output root and a list of root-relative paths of
234 unzipped Java files
235 """
236 srcjar_java_files = []
237 if source_jars:
238 tmp_input_root = tempfile.mkdtemp()
239 for source_jar in source_jars:
240 zip_ref = zipfile.ZipFile(source_jar, 'r')
241 zip_entries = []
242
243 for file_entry in zip_ref.namelist():
244 # We only care about Java source files.
245 if file_entry.endswith('.java'):
246 zip_entries.append(file_entry)
247
248 zip_ref.extractall(tmp_input_root, zip_entries)
249 zip_ref.close()
250 srcjar_java_files.extend(zip_entries)
251
252 return (tmp_input_root, srcjar_java_files)
253 else:
254 return None
255
256
257def RenameGenJarObjcFileRootInFileContent(tmp_objc_file_root,
258 j2objc_source_paths,
259 gen_src_jar, genjar_objc_files,
260 execute=subprocess.check_call):
261 """Renames references to temporary root inside ObjC sources from gen srcjar.
262
263 Args:
264 tmp_objc_file_root: The temporary output root containing ObjC sources.
265 j2objc_source_paths: The source paths used by J2ObjC.
266 gen_src_jar: The path of the gen srcjar.
267 genjar_objc_files: The list of ObjC sources translated from the gen srcjar.
268 execute: The function used to execute shell commands.
269 Returns:
270 None.
271 """
272 if genjar_objc_files:
273 abs_genjar_objc_source_files = [
274 os.path.join(tmp_objc_file_root, genjar_objc_f)
275 for genjar_objc_f in genjar_objc_files
276 ]
277 abs_genjar_objc_header_files = [
278 os.path.join(tmp_objc_file_root,
279 os.path.splitext(genjar_objc_f)[0] + '.h')
280 for genjar_objc_f in genjar_objc_files
281 ]
282
283 # We execute a command to change all references of the temporary Java root
284 # where we unzipped the gen srcjar sources, to the actual gen srcjar that
285 # contains the original Java sources.
286 cmd = [
287 'sed',
288 '-i',
289 '-e',
290 's|%s/|%s::|g' % (j2objc_source_paths[1], gen_src_jar)
291 ]
292 cmd.extend(abs_genjar_objc_source_files)
293 cmd.extend(abs_genjar_objc_header_files)
294 execute(cmd, stderr=subprocess.STDOUT)
295
296
297def MoveObjcFileToFinalOutputRoot(objc_files,
298 tmp_objc_file_root,
299 final_objc_file_root,
300 suffix,
301 os_module=os,
302 shutil_module=shutil):
303 """Moves ObjC files from temporary location to the final output location.
304
305 Args:
306 objc_files: The list of objc files to move.
307 tmp_objc_file_root: The temporary output root containing ObjC sources.
308 final_objc_file_root: The final output root.
309 suffix: The suffix of the files to move.
310 os_module: The os python module.
311 shutil_module: The shutil python module.
312 Returns:
313 None.
314 """
315 for objc_file in objc_files:
316 file_with_suffix = os_module.path.splitext(objc_file)[0] + suffix
317 dest_path = os_module.path.join(
318 final_objc_file_root, file_with_suffix)
319 dest_path_dir = os_module.path.dirname(dest_path)
320
321 if not os_module.path.isdir(dest_path_dir):
322 try:
323 os_module.makedirs(dest_path_dir)
324 except OSError as e:
325 if e.errno != errno.EEXIST or not os_module.path.isdir(dest_path_dir):
326 raise
327
328 shutil_module.move(
329 os_module.path.join(tmp_objc_file_root, file_with_suffix),
330 dest_path)
331
332
333def PostJ2ObjcFileProcessing(normal_objc_files, genjar_objc_files,
334 tmp_objc_file_root, final_objc_file_root,
335 j2objc_source_paths, gen_src_jar,
336 output_gen_source_dir, output_gen_header_dir):
337 """Performs cleanups on ObjC files and moves them to final output location.
338
339 Args:
340 normal_objc_files: The list of objc files translated from normal Java files.
341 genjar_objc_files: The list of ObjC sources translated from the gen srcjar.
342 tmp_objc_file_root: The temporary output root containing ObjC sources.
343 final_objc_file_root: The final output root.
344 j2objc_source_paths: The source paths used by J2ObjC.
345 gen_src_jar: The path of the gen srcjar.
346 output_gen_source_dir: The final output directory of ObjC source files
347 translated from gen srcjar. Maybe null.
348 output_gen_header_dir: The final output directory of ObjC header files
349 translated from gen srcjar. Maybe null.
350 Returns:
351 None.
352 """
353 RenameGenJarObjcFileRootInFileContent(tmp_objc_file_root,
354 j2objc_source_paths,
355 gen_src_jar,
356 genjar_objc_files)
357 MoveObjcFileToFinalOutputRoot(normal_objc_files,
358 tmp_objc_file_root,
359 final_objc_file_root,
360 '.m')
361 MoveObjcFileToFinalOutputRoot(normal_objc_files,
362 tmp_objc_file_root,
363 final_objc_file_root,
364 '.h')
365
366 if output_gen_source_dir:
367 MoveObjcFileToFinalOutputRoot(
368 genjar_objc_files,
369 tmp_objc_file_root,
370 output_gen_source_dir,
371 '.m')
372
373 if output_gen_header_dir:
374 MoveObjcFileToFinalOutputRoot(
375 genjar_objc_files,
376 tmp_objc_file_root,
377 output_gen_header_dir,
378 '.h')
379
380
381def GenerateJ2objcMappingFiles(normal_objc_files,
382 genjar_objc_files,
383 tmp_objc_file_root,
384 output_dependency_mapping_file,
385 output_archive_source_mapping_file,
386 compiled_archive_file_path):
387 """Generates J2ObjC mapping files.
388
389 Args:
390 normal_objc_files: The list of objc files translated from normal Java files.
391 genjar_objc_files: The list of ObjC sources translated from the gen srcjar.
392 tmp_objc_file_root: The temporary output root containing ObjC sources.
393 output_dependency_mapping_file: The path of the dependency mapping file to
394 write to.
395 output_archive_source_mapping_file: A path of the mapping file to write to.
396 compiled_archive_file_path: The path of the archive file.
397 Returns:
398 None.
399 """
400 WriteDepMappingFile(normal_objc_files + genjar_objc_files,
401 tmp_objc_file_root,
402 output_dependency_mapping_file)
403
404 if output_archive_source_mapping_file:
405 WriteArchiveSourceMappingFile(compiled_archive_file_path,
406 output_archive_source_mapping_file,
407 normal_objc_files + genjar_objc_files)
408
409
410def main():
411 parser = argparse.ArgumentParser(fromfile_prefix_chars='@')
412 parser.add_argument(
413 '--java',
414 required=True,
415 help='The path to the Java executable.')
416 parser.add_argument(
417 '--jvm_flags',
418 default='-Xss4m,-XX:+UseParallelGC',
419 help='A comma-separated list of flags to pass to the JVM.')
420 parser.add_argument(
421 '--j2objc',
422 required=True,
423 help='The path to the J2ObjC deploy jar.')
424 parser.add_argument(
425 '--main_class',
426 required=True,
427 help='The main class of the J2ObjC deploy jar to execute.')
428 # TODO(rduan): Remove, no longer needed.
429 parser.add_argument(
430 '--translated_source_files',
431 required=False,
432 help=('A comma-separated list of file paths where J2ObjC will write the '
433 'translated files to.'))
434 parser.add_argument(
435 '--output_dependency_mapping_file',
436 required=True,
437 help='The file path of the dependency mapping file to write to.')
438 parser.add_argument(
439 '--objc_file_path', '-d',
440 required=True,
441 help=('The file path which represents a directory where the generated '
442 'ObjC files reside.'))
443 parser.add_argument(
444 '--output_archive_source_mapping_file',
445 help='The file path of the mapping file containing mappings between the '
446 'translated source files and the to-be-generated archive file '
447 'compiled from those source files. --compile_archive_file_path must '
448 'be specified if this option is specified.')
449 parser.add_argument(
450 '--compiled_archive_file_path',
451 required=False,
452 help=('The archive file path that will be produced by ObjC compile action'
453 ' later'))
454 # TODO(rduan): Remove this flag once it is fully replaced by flag --src_jars.
455 parser.add_argument(
456 '--gen_src_jar',
457 required=False,
458 help='The jar containing Java sources generated by annotation processor.')
459 parser.add_argument(
460 '--src_jars',
461 required=False,
462 help='The list of Java source jars containing Java sources to translate.')
463 parser.add_argument(
464 '--output_gen_source_dir',
465 required=False,
466 help='The output directory of ObjC source files translated from the gen'
467 ' srcjar')
468 parser.add_argument(
469 '--output_gen_header_dir',
470 required=False,
471 help='The output directory of ObjC header files translated from the gen'
472 ' srcjar')
473
474 args, pass_through_args = parser.parse_known_args()
475 normal_java_files, j2objc_flags = _ParseArgs(pass_through_args)
476 srcjar_java_files = []
477 j2objc_source_paths = [os.getcwd()]
478
479 # Unzip the source jars, so J2ObjC can translate the contained sources.
480 # Also add the temporary directory containing the unzipped sources as a source
481 # path for J2ObjC, so it can find these sources.
482 source_jars = []
483 if args.gen_src_jar:
484 source_jars.append(args.gen_src_jar)
485 if args.src_jars:
486 source_jars.extend(args.src_jars.split(','))
487
488 srcjar_source_tuple = UnzipSourceJarSources(source_jars)
489 if srcjar_source_tuple:
490 j2objc_source_paths.append(srcjar_source_tuple[0])
491 srcjar_java_files = srcjar_source_tuple[1]
492
493 # Run J2ObjC over the normal input Java files and unzipped gen jar Java files.
494 # The output is stored in a temporary directory.
495 tmp_objc_file_root = tempfile.mkdtemp()
496
497 # If we do not generate the header mapping from J2ObjC, we still
498 # need to specify --output-header-mapping, as it signals to J2ObjC that we
499 # are using source paths as import paths, not package paths.
500 # TODO(rduan): Make another flag in J2ObjC to specify using source paths.
501 if '--output-header-mapping' not in j2objc_flags:
502 j2objc_flags.extend(['--output-header-mapping', '/dev/null'])
503
504 RunJ2ObjC(args.java,
505 args.jvm_flags,
506 args.j2objc,
507 args.main_class,
508 tmp_objc_file_root,
509 j2objc_flags,
510 j2objc_source_paths,
511 normal_java_files + srcjar_java_files)
512
513 # Calculate the relative paths of generated objc files.
514 normal_objc_files = _J2ObjcOutputObjcFiles(normal_java_files)
515 genjar_objc_files = _J2ObjcOutputObjcFiles(srcjar_java_files)
516
517 # Generate J2ObjC mapping files needed for distributed builds.
518 GenerateJ2objcMappingFiles(normal_objc_files,
519 genjar_objc_files,
520 tmp_objc_file_root,
521 args.output_dependency_mapping_file,
522 args.output_archive_source_mapping_file,
523 args.compiled_archive_file_path)
524
525 # Post J2ObjC-run processing, involving file editing, zipping and moving
526 # files to their final output locations.
527 PostJ2ObjcFileProcessing(
528 normal_objc_files,
529 genjar_objc_files,
530 tmp_objc_file_root,
531 args.objc_file_path,
532 j2objc_source_paths,
533 args.gen_src_jar,
534 args.output_gen_source_dir,
535 args.output_gen_header_dir)
536
537if __name__ == '__main__':
538 main()