blob: c88536e5f7ade9da1ce23e1c274078af4c6ee0a4 [file] [log] [blame]
#!/usr/bin/env python
#
# Copyright (C) 2019 The Android Open Source Project
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
"""Call cargo -v, parse its output, and generate Android.bp.
Usage: Run this script in a crate workspace root directory.
The Cargo.toml file should work at least for the host platform.
(1) Without other flags, "cargo2android.py --run"
calls cargo clean, calls cargo build -v, and generates Android.bp.
The cargo build only generates crates for the host,
without test crates.
(2) To build crates for both host and device in Android.bp, use the
--device flag, for example:
cargo2android.py --run --device
This is equivalent to using the --cargo flag to add extra builds:
cargo2android.py --run
--cargo "build"
--cargo "build --target x86_64-unknown-linux-gnu"
On MacOS, use x86_64-apple-darwin as target triple.
Here the host target triple is used as a fake cross compilation target.
If the crate's Cargo.toml and environment configuration works for an
Android target, use that target triple as the cargo build flag.
(3) To build default and test crates, for host and device, use both
--device and --tests flags:
cargo2android.py --run --device --tests
This is equivalent to using the --cargo flag to add extra builds:
cargo2android.py --run
--cargo "build"
--cargo "build --tests"
--cargo "build --target x86_64-unknown-linux-gnu"
--cargo "build --tests --target x86_64-unknown-linux-gnu"
Since Android rust builds by default treat all warnings as errors,
if there are rustc warning messages, this script will add
deny_warnings:false to the owner crate module in Android.bp.
"""
from __future__ import print_function
import argparse
import os
import os.path
import re
RENAME_MAP = {
# This map includes all changes to the default rust library module
# names to resolve name conflicts or avoid confusion.
'libbacktrace': 'libbacktrace_rust',
'libgcc': 'libgcc_rust',
'liblog': 'liblog_rust',
'libsync': 'libsync_rust',
'libx86_64': 'libx86_64_rust',
}
# Header added to all generated Android.bp files.
ANDROID_BP_HEADER = '// This file is generated by cargo2android.py.\n'
CARGO_OUT = 'cargo.out' # Name of file to keep cargo build -v output.
TARGET_TMP = 'target.tmp' # Name of temporary output directory.
# Message to be displayed when this script is called without the --run flag.
DRY_RUN_NOTE = (
'Dry-run: This script uses ./' + TARGET_TMP + ' for output directory,\n' +
'runs cargo clean, runs cargo build -v, saves output to ./cargo.out,\n' +
'and writes to Android.bp in the current and subdirectories.\n\n' +
'To do do all of the above, use the --run flag.\n' +
'See --help for other flags, and more usage notes in this script.\n')
# Cargo -v output of a call to rustc.
RUSTC_PAT = re.compile('^ +Running `rustc (.*)`$')
# Cargo -vv output of a call to rustc could be split into multiple lines.
# Assume that the first line will contain some CARGO_* env definition.
RUSTC_VV_PAT = re.compile('^ +Running `.*CARGO_.*=.*$')
# The combined -vv output rustc command line pattern.
RUSTC_VV_CMD_ARGS = re.compile('^ *Running `.*CARGO_.*=.* rustc (.*)`$')
# Cargo -vv output of a "cc" or "ar" command; all in one line.
CC_AR_VV_PAT = re.compile(r'^\[([^ ]*)[^\]]*\] running:? "(cc|ar)" (.*)$')
# Some package, such as ring-0.13.5, has pattern '... running "cc"'.
# Rustc output of file location path pattern for a warning message.
WARNING_FILE_PAT = re.compile('^ *--> ([^:]*):[0-9]+')
# Rust package name with suffix -d1.d2.d3.
VERSION_SUFFIX_PAT = re.compile(r'^(.*)-[0-9]+\.[0-9]+\.[0-9]+$')
def altered_name(name):
return RENAME_MAP[name] if (name in RENAME_MAP) else name
def is_build_crate_name(name):
# We added special prefix to build script crate names.
return name.startswith('build_script_')
def is_dependent_file_path(path):
# Absolute or dependent '.../' paths are not main files of this crate.
return path.startswith('/') or path.startswith('.../')
def get_module_name(crate): # to sort crates in a list
return crate.module_name
def pkg2crate_name(s):
return s.replace('-', '_').replace('.', '_')
def file_base_name(path):
return os.path.splitext(os.path.basename(path))[0]
def test_base_name(path):
return pkg2crate_name(file_base_name(path))
def unquote(s): # remove quotes around str
if s and len(s) > 1 and s[0] == '"' and s[-1] == '"':
return s[1:-1]
return s
def remove_version_suffix(s): # remove -d1.d2.d3 suffix
if VERSION_SUFFIX_PAT.match(s):
return VERSION_SUFFIX_PAT.match(s).group(1)
return s
def short_out_name(pkg, s): # replace /.../pkg-*/out/* with .../out/*
return re.sub('^/.*/' + pkg + '-[0-9a-f]*/out/', '.../out/', s)
def escape_quotes(s): # replace '"' with '\\"'
return s.replace('"', '\\"')
class Crate(object):
"""Information of a Rust crate to collect/emit for an Android.bp module."""
def __init__(self, runner, outf_name):
# Remembered global runner and its members.
self.runner = runner
self.debug = runner.args.debug
self.cargo_dir = '' # directory of my Cargo.toml
self.outf_name = outf_name # path to Android.bp
self.outf = None # open file handle of outf_name during dump*
# Variants/results that could be merged from multiple rustc lines.
self.host_supported = False
self.device_supported = False
self.has_warning = False
# Android module properties derived from rustc parameters.
self.module_name = '' # unique in Android build system
self.module_type = '' # rust_{binary,library,test}[_host] etc.
self.root_pkg = '' # parent package name of a sub/test packge, from -L
self.srcs = list() # main_src or merged multiple source files
self.stem = '' # real base name of output file
# Kept parsed status
self.errors = '' # all errors found during parsing
self.line_num = 1 # runner told input source line number
self.line = '' # original rustc command line parameters
# Parameters collected from rustc command line.
self.crate_name = '' # follows --crate-name
self.main_src = '' # follows crate_name parameter, shortened
self.crate_type = '' # bin|lib|test (see --test flag)
self.cfgs = list() # follows --cfg, without feature= prefix
self.features = list() # follows --cfg, name in 'feature="..."'
self.codegens = list() # follows -C, some ignored
self.externs = list() # follows --extern
self.core_externs = list() # first part of self.externs elements
self.static_libs = list() # e.g. -l static=host_cpuid
self.shared_libs = list() # e.g. -l dylib=wayland-client, -l z
self.cap_lints = '' # follows --cap-lints
self.emit_list = '' # e.g., --emit=dep-info,metadata,link
self.edition = '2015' # rustc default, e.g., --edition=2018
self.target = '' # follows --target
def write(self, s):
# convenient way to output one line at a time with EOL.
self.outf.write(s + '\n')
def same_flags(self, other):
# host_supported, device_supported, has_warning are not compared but merged
# target is not compared, to merge different target/host modules
# externs is not compared; only core_externs is compared
return (not self.errors and not other.errors and
self.edition == other.edition and
self.cap_lints == other.cap_lints and
self.emit_list == other.emit_list and
self.core_externs == other.core_externs and
self.codegens == other.codegens and
self.features == other.features and
self.static_libs == other.static_libs and
self.shared_libs == other.shared_libs and self.cfgs == other.cfgs)
def merge_host_device(self, other):
"""Returns true if attributes are the same except host/device support."""
return (self.crate_name == other.crate_name and
self.crate_type == other.crate_type and
self.main_src == other.main_src and self.stem == other.stem and
self.root_pkg == other.root_pkg and not self.skip_crate() and
self.same_flags(other))
def merge_test(self, other):
"""Returns true if self and other are tests of same root_pkg."""
# Before merger, each test has its own crate_name.
# A merged test uses its source file base name as output file name,
# so a test is mergeable only if its base name equals to its crate name.
return (self.crate_type == other.crate_type and
self.crate_type == 'test' and self.root_pkg == other.root_pkg and
not self.skip_crate() and
other.crate_name == test_base_name(other.main_src) and
(len(self.srcs) > 1 or
(self.crate_name == test_base_name(self.main_src)) and
self.host_supported == other.host_supported and
self.device_supported == other.device_supported) and
self.same_flags(other))
def merge(self, other, outf_name):
"""Try to merge crate into self."""
should_merge_host_device = self.merge_host_device(other)
should_merge_test = False
if not should_merge_host_device:
should_merge_test = self.merge_test(other)
# A for-device test crate can be merged with its for-host version,
# or merged with a different test for the same host or device.
# Since we run cargo once for each device or host, test crates for the
# first device or host will be merged first. Then test crates for a
# different device or host should be allowed to be merged into a
# previously merged one, maybe for a different device or host.
if should_merge_host_device or should_merge_test:
self.runner.init_bp_file(outf_name)
with open(outf_name, 'a') as outf: # to write debug info
self.outf = outf
other.outf = outf
self.do_merge(other, should_merge_test)
return True
return False
def do_merge(self, other, should_merge_test):
"""Merge attributes of other to self."""
if self.debug:
self.write('\n// Before merge definition (1):')
self.dump_debug_info()
self.write('\n// Before merge definition (2):')
other.dump_debug_info()
# Merge properties of other to self.
self.host_supported = self.host_supported or other.host_supported
self.device_supported = self.device_supported or other.device_supported
self.has_warning = self.has_warning or other.has_warning
if not self.target: # okay to keep only the first target triple
self.target = other.target
# decide_module_type sets up default self.stem,
# which can be changed if self is a merged test module.
self.decide_module_type()
if should_merge_test:
self.srcs.append(other.main_src)
# use a short unique name as the merged module name.
prefix = self.root_pkg + '_tests'
self.module_name = self.runner.claim_module_name(prefix, self, 0)
self.stem = self.module_name
# This normalized root_pkg name although might be the same
# as other module's crate_name, it is not actually used for
# output file name. A merged test module always have multiple
# source files and each source file base name is used as
# its output file name.
self.crate_name = pkg2crate_name(self.root_pkg)
if self.debug:
self.write('\n// After merge definition (1):')
self.dump_debug_info()
def find_cargo_dir(self):
"""Deepest directory with Cargo.toml and contains the main_src."""
if not is_dependent_file_path(self.main_src):
dir_name = os.path.dirname(self.main_src)
while dir_name:
if os.path.exists(dir_name + '/Cargo.toml'):
self.cargo_dir = dir_name
return
dir_name = os.path.dirname(dir_name)
def parse(self, line_num, line):
"""Find important rustc arguments to convert to Android.bp properties."""
self.line_num = line_num
self.line = line
args = line.split() # Loop through every argument of rustc.
i = 0
while i < len(args):
arg = args[i]
if arg == '--crate-name':
self.crate_name = args[i + 1]
i += 2
# shorten imported crate main source path
self.main_src = re.sub('^/[^ ]*/registry/src/', '.../', args[i])
self.main_src = re.sub('^.../github.com-[0-9a-f]*/', '.../',
self.main_src)
self.find_cargo_dir()
if self.cargo_dir and not self.runner.args.onefile:
# Write to Android.bp in the subdirectory with Cargo.toml.
self.outf_name = self.cargo_dir + '/Android.bp'
self.main_src = self.main_src[len(self.cargo_dir) + 1:]
elif arg == '--crate-type':
i += 1
if self.crate_type:
self.errors += ' ERROR: multiple --crate-type '
self.errors += self.crate_type + ' ' + args[i] + '\n'
# TODO(chh): handle multiple types, e.g. lexical-core-0.4.6 has
# crate-type = ["lib", "staticlib", "cdylib"]
# output: debug/liblexical_core.{a,so,rlib}
# cargo calls rustc with multiple --crate-type flags.
# rustc can accept:
# --crate-type [bin|lib|rlib|dylib|cdylib|staticlib|proc-macro]
# Comma separated list of types of crates for the compiler to emit
self.crate_type = args[i]
elif arg == '--test':
# only --test or --crate-type should appear once
if self.crate_type:
self.errors += (' ERROR: found both --test and --crate-type ' +
self.crate_type + '\n')
else:
self.crate_type = 'test'
elif arg == '--target':
i += 1
self.target = args[i]
elif arg == '--cfg':
i += 1
if args[i].startswith('\'feature='):
self.features.append(unquote(args[i].replace('\'feature=', '')[:-1]))
else:
self.cfgs.append(args[i])
elif arg == '--extern':
i += 1
extern_names = re.sub('=/[^ ]*/deps/', ' = ', args[i])
self.externs.append(extern_names)
self.core_externs.append(re.sub(' = .*', '', extern_names))
elif arg == '-C': # codegen options
i += 1
# ignore options not used in Android
if not (args[i].startswith('debuginfo=') or
args[i].startswith('extra-filename=') or
args[i].startswith('incremental=') or
args[i].startswith('metadata=')):
self.codegens.append(args[i])
elif arg == '--cap-lints':
i += 1
self.cap_lints = args[i]
elif arg == '-L':
i += 1
if args[i].startswith('dependency=') and args[i].endswith('/deps'):
if '/' + TARGET_TMP + '/' in args[i]:
self.root_pkg = re.sub(
'^.*/', '', re.sub('/' + TARGET_TMP + '/.*/deps$', '', args[i]))
else:
self.root_pkg = re.sub('^.*/', '',
re.sub('/[^/]+/[^/]+/deps$', '', args[i]))
self.root_pkg = remove_version_suffix(self.root_pkg)
elif arg == '-l':
i += 1
if args[i].startswith('static='):
self.static_libs.append(re.sub('static=', '', args[i]))
elif args[i].startswith('dylib='):
self.shared_libs.append(re.sub('dylib=', '', args[i]))
else:
self.shared_libs.append(args[i])
elif arg == '--out-dir' or arg == '--color': # ignored
i += 1
elif arg.startswith('--error-format=') or arg.startswith('--json='):
_ = arg # ignored
elif arg.startswith('--emit='):
self.emit_list = arg.replace('--emit=', '')
elif arg.startswith('--edition='):
self.edition = arg.replace('--edition=', '')
else:
self.errors += 'ERROR: unknown ' + arg + '\n'
i += 1
if not self.crate_name:
self.errors += 'ERROR: missing --crate-name\n'
if not self.main_src:
self.errors += 'ERROR: missing main source file\n'
else:
self.srcs.append(self.main_src)
if not self.crate_type:
# Treat "--cfg test" as "--test"
if 'test' in self.cfgs:
self.crate_type = 'test'
else:
self.errors += 'ERROR: missing --crate-type\n'
if not self.root_pkg:
self.root_pkg = self.crate_name
if self.target:
self.device_supported = True
self.host_supported = True # assume host supported for all builds
self.cfgs = sorted(set(self.cfgs))
self.features = sorted(set(self.features))
self.codegens = sorted(set(self.codegens))
self.externs = sorted(set(self.externs))
self.core_externs = sorted(set(self.core_externs))
self.static_libs = sorted(set(self.static_libs))
self.shared_libs = sorted(set(self.shared_libs))
self.decide_module_type()
self.module_name = altered_name(self.stem)
return self
def dump_line(self):
self.write('\n// Line ' + str(self.line_num) + ' ' + self.line)
def feature_list(self):
"""Return a string of main_src + "feature_list"."""
pkg = self.main_src
if pkg.startswith('.../'): # keep only the main package name
pkg = re.sub('/.*', '', pkg[4:])
if not self.features:
return pkg
return pkg + ' "' + ','.join(self.features) + '"'
def dump_skip_crate(self, kind):
if self.debug:
self.write('\n// IGNORED: ' + kind + ' ' + self.main_src)
return self
def skip_crate(self):
"""Return crate_name or a message if this crate should be skipped."""
if is_build_crate_name(self.crate_name):
return self.crate_name
if is_dependent_file_path(self.main_src):
return 'dependent crate'
return ''
def dump(self):
"""Dump all error/debug/module code to the output .bp file."""
self.runner.init_bp_file(self.outf_name)
with open(self.outf_name, 'a') as outf:
self.outf = outf
if self.errors:
self.dump_line()
self.write(self.errors)
elif self.skip_crate():
self.dump_skip_crate(self.skip_crate())
else:
if self.debug:
self.dump_debug_info()
self.dump_android_module()
def dump_debug_info(self):
"""Dump parsed data, when cargo2android is called with --debug."""
def dump(name, value):
self.write('//%12s = %s' % (name, value))
def opt_dump(name, value):
if value:
dump(name, value)
def dump_list(fmt, values):
for v in values:
self.write(fmt % v)
self.dump_line()
dump('module_name', self.module_name)
dump('crate_name', self.crate_name)
dump('crate_type', self.crate_type)
dump('main_src', self.main_src)
dump('has_warning', self.has_warning)
dump('for_host', self.host_supported)
dump('for_device', self.device_supported)
dump('module_type', self.module_type)
opt_dump('target', self.target)
opt_dump('edition', self.edition)
opt_dump('emit_list', self.emit_list)
opt_dump('cap_lints', self.cap_lints)
dump_list('// cfg = %s', self.cfgs)
dump_list('// cfg = \'feature "%s"\'', self.features)
# TODO(chh): escape quotes in self.features, but not in other dump_list
dump_list('// codegen = %s', self.codegens)
dump_list('// externs = %s', self.externs)
dump_list('// -l static = %s', self.static_libs)
dump_list('// -l (dylib) = %s', self.shared_libs)
def dump_android_module(self):
"""Dump one Android module definition."""
if not self.module_type:
self.write('\nERROR: unknown crate_type ' + self.crate_type)
return
self.write('\n' + self.module_type + ' {')
self.dump_android_core_properties()
if self.edition:
self.write(' edition: "' + self.edition + '",')
self.dump_android_property_list('features', '"%s"', self.features)
cfg_fmt = '"--cfg %s"'
if self.cap_lints:
allowed = '"--cap-lints ' + self.cap_lints + '"'
if not self.cfgs:
self.write(' flags: [' + allowed + '],')
else:
self.write(' flags: [\n ' + allowed + ',')
self.dump_android_property_list_items(cfg_fmt, self.cfgs)
self.write(' ],')
else:
self.dump_android_property_list('flags', cfg_fmt, self.cfgs)
if self.externs:
self.dump_android_externs()
self.dump_android_property_list('static_libs', '"lib%s"', self.static_libs)
self.dump_android_property_list('shared_libs', '"lib%s"', self.shared_libs)
self.write('}')
def test_module_name(self):
"""Return a unique name for a test module."""
# root_pkg+'_tests_'+(crate_name|source_file_path)
suffix = self.crate_name
if not suffix:
suffix = re.sub('/', '_', re.sub('.rs$', '', self.main_src))
return self.root_pkg + '_tests_' + suffix
def decide_module_type(self):
"""Decide which Android module type to use."""
host = '' if self.device_supported else '_host'
if self.crate_type == 'bin': # rust_binary[_host]
self.module_type = 'rust_binary' + host
self.stem = self.crate_name
elif self.crate_type == 'lib': # rust_library[_host]_rlib
self.module_type = 'rust_library' + host + '_rlib'
self.stem = 'lib' + self.crate_name
elif self.crate_type == 'cdylib': # rust_library[_host]_dylib
# TODO(chh): complete and test cdylib module type
self.module_type = 'rust_library' + host + '_dylib'
self.stem = 'lib' + self.crate_name + '.so'
elif self.crate_type == 'test': # rust_test[_host]
self.module_type = 'rust_test' + host
self.stem = self.test_module_name()
# self.stem will be changed after merging with other tests.
# self.stem is NOT used for final test binary name.
# rust_test uses each source file base name as its output file name,
# unless crate_name is specified by user in Cargo.toml.
elif self.crate_type == 'proc-macro': # rust_proc_macro
self.module_type = 'rust_proc_macro'
self.stem = 'lib' + self.crate_name
else: # unknown module type, rust_prebuilt_dylib? rust_library[_host]?
self.module_type = ''
self.stem = ''
def dump_android_property_list_items(self, fmt, values):
for v in values:
# fmt has quotes, so we need escape_quotes(v)
self.write(' ' + (fmt % escape_quotes(v)) + ',')
def dump_android_property_list(self, name, fmt, values):
if values:
self.write(' ' + name + ': [')
self.dump_android_property_list_items(fmt, values)
self.write(' ],')
def dump_android_core_properties(self):
"""Dump the module header, name, stem, etc."""
self.write(' name: "' + self.module_name + '",')
if self.stem != self.module_name:
self.write(' stem: "' + self.stem + '",')
if self.has_warning and not self.cap_lints:
self.write(' deny_warnings: false,')
if self.host_supported and self.device_supported:
self.write(' host_supported: true,')
self.write(' crate_name: "' + self.crate_name + '",')
if len(self.srcs) > 1:
self.srcs = sorted(set(self.srcs))
self.dump_android_property_list('srcs', '"%s"', self.srcs)
else:
self.write(' srcs: ["' + self.main_src + '"],')
if self.crate_type == 'test':
# self.root_pkg can have multiple test modules, with different *_tests[n]
# names, but their executables can all be installed under the same _tests
# directory. When built from Cargo.toml, all tests should have different
# file or crate names.
self.write(' relative_install_path: "' + self.root_pkg + '_tests",')
self.write(' test_suites: ["general-tests"],')
self.write(' auto_gen_config: true,')
def dump_android_externs(self):
"""Dump the dependent rlibs and dylibs property."""
so_libs = list()
rust_libs = ''
deps_libname = re.compile('^.* = lib(.*)-[0-9a-f]*.(rlib|so|rmeta)$')
for lib in self.externs:
# normal value of lib: "libc = liblibc-*.rlib"
# strange case in rand crate: "getrandom_package = libgetrandom-*.rlib"
# we should use "libgetrandom", not "lib" + "getrandom_package"
groups = deps_libname.match(lib)
if groups is not None:
lib_name = groups.group(1)
else:
lib_name = re.sub(' .*$', '', lib)
if lib.endswith('.rlib') or lib.endswith('.rmeta'):
# On MacOS .rmeta is used when Linux uses .rlib or .rmeta.
rust_libs += ' "' + altered_name('lib' + lib_name) + '",\n'
elif lib.endswith('.so'):
so_libs.append(lib_name)
else:
rust_libs += ' // ERROR: unknown type of lib ' + lib_name + '\n'
if rust_libs:
self.write(' rlibs: [\n' + rust_libs + ' ],')
# Are all dependent .so files proc_macros?
# TODO(chh): Separate proc_macros and dylib.
self.dump_android_property_list('proc_macros', '"lib%s"', so_libs)
class ARObject(object):
"""Information of an "ar" link command."""
def __init__(self, runner, outf_name):
# Remembered global runner and its members.
self.runner = runner
self.pkg = ''
self.outf_name = outf_name # path to Android.bp
# "ar" arguments
self.line_num = 1
self.line = ''
self.flags = '' # e.g. "crs"
self.lib = '' # e.g. "/.../out/lib*.a"
self.objs = list() # e.g. "/.../out/.../*.o"
def parse(self, pkg, line_num, args_line):
"""Collect ar obj/lib file names."""
self.pkg = pkg
self.line_num = line_num
self.line = args_line
args = args_line.split()
num_args = len(args)
if num_args < 3:
print('ERROR: "ar" command has too few arguments', args_line)
else:
self.flags = unquote(args[0])
self.lib = unquote(args[1])
self.objs = sorted(set(map(unquote, args[2:])))
return self
def write(self, s):
self.outf.write(s + '\n')
def dump_debug_info(self):
self.write('\n// Line ' + str(self.line_num) + ' "ar" ' + self.line)
self.write('// ar_object for %12s' % self.pkg)
self.write('// flags = %s' % self.flags)
self.write('// lib = %s' % short_out_name(self.pkg, self.lib))
for o in self.objs:
self.write('// obj = %s' % short_out_name(self.pkg, o))
def dump_android_lib(self):
"""Write cc_library_static into Android.bp."""
self.write('\ncc_library_static {')
self.write(' name: "' + file_base_name(self.lib) + '",')
self.write(' host_supported: true,')
if self.flags != 'crs':
self.write(' // ar flags = %s' % self.flags)
if self.pkg not in self.runner.pkg_obj2cc:
self.write(' ERROR: cannot find source files.\n}')
return
self.write(' srcs: [')
obj2cc = self.runner.pkg_obj2cc[self.pkg]
# Note: wflags are ignored.
dflags = list()
fflags = list()
for obj in self.objs:
self.write(' "' + short_out_name(self.pkg, obj2cc[obj].src) + '",')
# TODO(chh): union of dflags and flags of all obj
# Now, just a temporary hack that uses the last obj's flags
dflags = obj2cc[obj].dflags
fflags = obj2cc[obj].fflags
self.write(' ],')
self.write(' cflags: [')
self.write(' "-O3",') # TODO(chh): is this default correct?
self.write(' "-Wno-error",')
for x in fflags:
self.write(' "-f' + x + '",')
for x in dflags:
self.write(' "-D' + x + '",')
self.write(' ],')
self.write('}')
def dump(self):
"""Dump error/debug/module info to the output .bp file."""
self.runner.init_bp_file(self.outf_name)
with open(self.outf_name, 'a') as outf:
self.outf = outf
if self.runner.args.debug:
self.dump_debug_info()
self.dump_android_lib()
class CCObject(object):
"""Information of a "cc" compilation command."""
def __init__(self, runner, outf_name):
# Remembered global runner and its members.
self.runner = runner
self.pkg = ''
self.outf_name = outf_name # path to Android.bp
# "cc" arguments
self.line_num = 1
self.line = ''
self.src = ''
self.obj = ''
self.dflags = list() # -D flags
self.fflags = list() # -f flags
self.iflags = list() # -I flags
self.wflags = list() # -W flags
self.other_args = list()
def parse(self, pkg, line_num, args_line):
"""Collect cc compilation flags and src/out file names."""
self.pkg = pkg
self.line_num = line_num
self.line = args_line
args = args_line.split()
i = 0
while i < len(args):
arg = args[i]
if arg == '"-c"':
i += 1
if args[i].startswith('"-o'):
# ring-0.13.5 dumps: ... "-c" "-o/.../*.o" ".../*.c"
self.obj = unquote(args[i])[2:]
i += 1
self.src = unquote(args[i])
else:
self.src = unquote(args[i])
elif arg == '"-o"':
i += 1
self.obj = unquote(args[i])
elif arg == '"-I"':
i += 1
self.iflags.append(unquote(args[i]))
elif arg.startswith('"-D'):
self.dflags.append(unquote(args[i])[2:])
elif arg.startswith('"-f'):
self.fflags.append(unquote(args[i])[2:])
elif arg.startswith('"-W'):
self.wflags.append(unquote(args[i])[2:])
elif not (arg.startswith('"-O') or arg == '"-m64"' or arg == '"-g"' or
arg == '"-g3"'):
# ignore -O -m64 -g
self.other_args.append(unquote(args[i]))
i += 1
self.dflags = sorted(set(self.dflags))
self.fflags = sorted(set(self.fflags))
# self.wflags is not sorted because some are order sensitive
# and we ignore them anyway.
if self.pkg not in self.runner.pkg_obj2cc:
self.runner.pkg_obj2cc[self.pkg] = {}
self.runner.pkg_obj2cc[self.pkg][self.obj] = self
return self
def write(self, s):
self.outf.write(s + '\n')
def dump_debug_flags(self, name, flags):
self.write('// ' + name + ':')
for f in flags:
self.write('// %s' % f)
def dump(self):
"""Dump only error/debug info to the output .bp file."""
if not self.runner.args.debug:
return
self.runner.init_bp_file(self.outf_name)
with open(self.outf_name, 'a') as outf:
self.outf = outf
self.write('\n// Line ' + str(self.line_num) + ' "cc" ' + self.line)
self.write('// cc_object for %12s' % self.pkg)
self.write('// src = %s' % short_out_name(self.pkg, self.src))
self.write('// obj = %s' % short_out_name(self.pkg, self.obj))
self.dump_debug_flags('-I flags', self.iflags)
self.dump_debug_flags('-D flags', self.dflags)
self.dump_debug_flags('-f flags', self.fflags)
self.dump_debug_flags('-W flags', self.wflags)
if self.other_args:
self.dump_debug_flags('other args', self.other_args)
class Runner(object):
"""Main class to parse cargo -v output and print Android module definitions."""
def __init__(self, args):
self.bp_files = set() # Remember all output Android.bp files.
self.root_pkg = '' # name of package in ./Cargo.toml
# Saved flags, modes, and data.
self.args = args
self.dry_run = not args.run
self.skip_cargo = args.skipcargo
# All cc/ar objects, crates, dependencies, and warning files
self.cc_objects = list()
self.pkg_obj2cc = {}
# pkg_obj2cc[cc_object[i].pkg][cc_objects[i].obj] = cc_objects[i]
self.ar_objects = list()
self.crates = list()
self.dependencies = list() # dependent and build script crates
self.warning_files = set()
# Keep a unique mapping from (module name) to crate
self.name_owners = {}
# Default action is cargo clean, followed by build or user given actions.
if args.cargo:
self.cargo = ['clean'] + args.cargo
else:
self.cargo = ['clean', 'build']
default_target = '--target x86_64-unknown-linux-gnu'
if args.device:
self.cargo.append('build ' + default_target)
if args.tests:
self.cargo.append('build --tests')
self.cargo.append('build --tests ' + default_target)
elif args.tests:
self.cargo.append('build --tests')
def init_bp_file(self, name):
if name not in self.bp_files:
self.bp_files.add(name)
with open(name, 'w') as outf:
outf.write(ANDROID_BP_HEADER)
def claim_module_name(self, prefix, owner, counter):
"""Return prefix if not owned yet, otherwise, prefix+str(counter)."""
while True:
name = prefix
if counter > 0:
name += str(counter)
if name not in self.name_owners:
self.name_owners[name] = owner
return name
if owner == self.name_owners[name]:
return name
counter += 1
def find_root_pkg(self):
"""Read name of [package] in ./Cargo.toml."""
if not os.path.exists('./Cargo.toml'):
return
with open('./Cargo.toml', 'r') as inf:
pkg_section = re.compile(r'^ *\[package\]')
name = re.compile('^ *name *= * "([^"]*)"')
in_pkg = False
for line in inf:
if in_pkg:
if name.match(line):
self.root_pkg = name.match(line).group(1)
break
else:
in_pkg = pkg_section.match(line) is not None
def run_cargo(self):
"""Calls cargo -v and save its output to ./cargo.out."""
if self.skip_cargo:
return self
cargo = './Cargo.toml'
if not os.access(cargo, os.R_OK):
print('ERROR: Cannot find or read', cargo)
return self
if not self.dry_run and os.path.exists('cargo.out'):
os.remove('cargo.out')
cmd_tail = ' --target-dir ' + TARGET_TMP + ' >> cargo.out 2>&1'
for c in self.cargo:
features = ''
if self.args.features and c != 'clean':
features = ' --features ' + self.args.features
cmd = 'cargo -vv ' if self.args.vv else 'cargo -v '
cmd += c + features + cmd_tail
if self.args.rustflags and c != 'clean':
cmd = 'RUSTFLAGS="' + self.args.rustflags + '" ' + cmd
if self.dry_run:
print('Dry-run skip:', cmd)
else:
if self.args.verbose:
print('Running:', cmd)
with open('cargo.out', 'a') as cargo_out:
cargo_out.write('### Running: ' + cmd + '\n')
os.system(cmd)
return self
def dump_dependencies(self):
"""Append dependencies and their features to Android.bp."""
if not self.dependencies:
return
dependent_list = list()
for c in self.dependencies:
dependent_list.append(c.feature_list())
sorted_dependencies = sorted(set(dependent_list))
self.init_bp_file('Android.bp')
with open('Android.bp', 'a') as outf:
outf.write('\n// dependent_library ["feature_list"]\n')
for s in sorted_dependencies:
outf.write('// ' + s + '\n')
def dump_pkg_obj2cc(self):
"""Dump debug info of the pkg_obj2cc map."""
if not self.args.debug:
return
self.init_bp_file('Android.bp')
with open('Android.bp', 'a') as outf:
sorted_pkgs = sorted(self.pkg_obj2cc.keys())
for pkg in sorted_pkgs:
if not self.pkg_obj2cc[pkg]:
continue
outf.write('\n// obj => src for %s\n' % pkg)
obj2cc = self.pkg_obj2cc[pkg]
for obj in sorted(obj2cc.keys()):
outf.write('// ' + short_out_name(pkg, obj) + ' => ' +
short_out_name(pkg, obj2cc[obj].src) + '\n')
def gen_bp(self):
"""Parse cargo.out and generate Android.bp files."""
if self.dry_run:
print('Dry-run skip: read', CARGO_OUT, 'write Android.bp')
elif os.path.exists(CARGO_OUT):
self.find_root_pkg()
with open(CARGO_OUT, 'r') as cargo_out:
self.parse(cargo_out, 'Android.bp')
self.crates.sort(key=get_module_name)
for obj in self.cc_objects:
obj.dump()
self.dump_pkg_obj2cc()
for crate in self.crates:
crate.dump()
dumped_libs = set()
for lib in self.ar_objects:
if lib.pkg == self.root_pkg:
lib_name = file_base_name(lib.lib)
if lib_name not in dumped_libs:
dumped_libs.add(lib_name)
lib.dump()
if self.args.dependencies and self.dependencies:
self.dump_dependencies()
return self
def add_ar_object(self, obj):
self.ar_objects.append(obj)
def add_cc_object(self, obj):
self.cc_objects.append(obj)
def add_crate(self, crate):
"""Merge crate with someone in crates, or append to it. Return crates."""
if crate.skip_crate():
if self.args.debug: # include debug info of all crates
self.crates.append(crate)
if self.args.dependencies: # include only dependent crates
if (is_dependent_file_path(crate.main_src) and
not is_build_crate_name(crate.crate_name)):
self.dependencies.append(crate)
else:
for c in self.crates:
if c.merge(crate, 'Android.bp'):
return
self.crates.append(crate)
def find_warning_owners(self):
"""For each warning file, find its owner crate."""
missing_owner = False
for f in self.warning_files:
cargo_dir = '' # find lowest crate, with longest path
owner = None # owner crate of this warning
for c in self.crates:
if (f.startswith(c.cargo_dir + '/') and
len(cargo_dir) < len(c.cargo_dir)):
cargo_dir = c.cargo_dir
owner = c
if owner:
owner.has_warning = True
else:
missing_owner = True
if missing_owner and os.path.exists('Cargo.toml'):
# owner is the root cargo, with empty cargo_dir
for c in self.crates:
if not c.cargo_dir:
c.has_warning = True
def rustc_command(self, n, rustc_line, line, outf_name):
"""Process a rustc command line from cargo -vv output."""
# cargo build -vv output can have multiple lines for a rustc command
# due to '\n' in strings for environment variables.
# strip removes leading spaces and '\n' at the end
new_rustc = (rustc_line.strip() + line) if rustc_line else line
# Use an heuristic to detect the completions of a multi-line command.
# This might fail for some very rare case, but easy to fix manually.
if not line.endswith('`\n') or (new_rustc.count('`') % 2) != 0:
return new_rustc
if RUSTC_VV_CMD_ARGS.match(new_rustc):
args = RUSTC_VV_CMD_ARGS.match(new_rustc).group(1)
self.add_crate(Crate(self, outf_name).parse(n, args))
else:
self.assert_empty_vv_line(new_rustc)
return ''
def cc_ar_command(self, n, groups, outf_name):
pkg = groups.group(1)
line = groups.group(3)
if groups.group(2) == 'cc':
self.add_cc_object(CCObject(self, outf_name).parse(pkg, n, line))
else:
self.add_ar_object(ARObject(self, outf_name).parse(pkg, n, line))
def assert_empty_vv_line(self, line):
if line: # report error if line is not empty
self.init_bp_file('Android.bp')
with open('Android.bp', 'a') as outf:
outf.write('ERROR -vv line: ', line)
return ''
def parse(self, inf, outf_name):
"""Parse rustc and warning messages in inf, return a list of Crates."""
n = 0 # line number
prev_warning = False # true if the previous line was warning: ...
rustc_line = '' # previous line(s) matching RUSTC_VV_PAT
for line in inf:
n += 1
if line.startswith('warning: '):
prev_warning = True
rustc_line = self.assert_empty_vv_line(rustc_line)
continue
new_rustc = ''
if RUSTC_PAT.match(line):
args_line = RUSTC_PAT.match(line).group(1)
self.add_crate(Crate(self, outf_name).parse(n, args_line))
self.assert_empty_vv_line(rustc_line)
elif rustc_line or RUSTC_VV_PAT.match(line):
new_rustc = self.rustc_command(n, rustc_line, line, outf_name)
elif CC_AR_VV_PAT.match(line):
self.cc_ar_command(n, CC_AR_VV_PAT.match(line), outf_name)
elif prev_warning and WARNING_FILE_PAT.match(line):
self.assert_empty_vv_line(rustc_line)
fpath = WARNING_FILE_PAT.match(line).group(1)
if fpath[0] != '/': # ignore absolute path
self.warning_files.add(fpath)
prev_warning = False
rustc_line = new_rustc
self.find_warning_owners()
def parse_args():
"""Parse main arguments."""
parser = argparse.ArgumentParser('cargo2android')
parser.add_argument(
'--cargo',
action='append',
metavar='args_string',
help=('extra cargo build -v args in a string, ' +
'each --cargo flag calls cargo build -v once'))
parser.add_argument(
'--debug',
action='store_true',
default=False,
help='dump debug info into Android.bp')
parser.add_argument(
'--dependencies',
action='store_true',
default=False,
help='dump debug info of dependent crates')
parser.add_argument(
'--device',
action='store_true',
default=False,
help='run cargo also for a default device target')
parser.add_argument(
'--features', type=str, help='passing features to cargo build')
parser.add_argument(
'--onefile',
action='store_true',
default=False,
help=('output all into one ./Android.bp, default will generate ' +
'one Android.bp per Cargo.toml in subdirectories'))
parser.add_argument(
'--run',
action='store_true',
default=False,
help='run it, default is dry-run')
parser.add_argument('--rustflags', type=str, help='passing flags to rustc')
parser.add_argument(
'--skipcargo',
action='store_true',
default=False,
help='skip cargo command, parse cargo.out, and generate Android.bp')
parser.add_argument(
'--tests',
action='store_true',
default=False,
help='run cargo build --tests after normal build')
parser.add_argument(
'--verbose',
action='store_true',
default=False,
help='echo executed commands')
parser.add_argument(
'--vv',
action='store_true',
default=False,
help='run cargo with -vv instead of default -v')
return parser.parse_args()
def main():
args = parse_args()
if not args.run: # default is dry-run
print(DRY_RUN_NOTE)
Runner(args).run_cargo().gen_bp()
if __name__ == '__main__':
main()