Files
phs_v1.0.1.0/build/scripts/cargo2gn.py
2024-09-27 19:16:49 +08:00

1122 lines
48 KiB
Python

#!/usr/bin/env python3
# -*- coding: utf-8 -*-
# Copyright (c) 2023 Huawei Device Co., Ltd.
# 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.
from __future__ import print_function
import os
import os.path
import sys
import argparse
import glob
import json
import re
import shutil
# Rust path
RUST_PATH = '//third_party/rust/'
# import content added to all generated BUILD.gn files.
IMPORT_CONTENT = '//build/templates/rust/ohos.gni'
# The name of the temporary output directory.
TARGET_TEMP = 'target_temp'
# Header added to all generated BUILD.gn files.
BUILD_GN_HEADER = (
'# Copyright (c) 2023 Huawei Device Co., Ltd.\n' +
'# Licensed under the Apache License, Version 2.0 (the "License");\n' +
'# you may not use this file except in compliance with the License.\n' +
'# You may obtain a copy of the License at\n' +
'#\n' +
'# http://www.apache.org/licenses/LICENSE-2.0\n' +
'#\n' +
'# Unless required by applicable law or agreed to in writing, software\n' +
'# distributed under the License is distributed on an "AS IS" BASIS,\n' +
'# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n' +
'# See the License for the specific language governing permissions and\n' +
'# limitations under the License.\n')
# Message to be displayed when this script is called without the --run flag.
DRY_RUN_CONTENT = (
'Dry-run: This script uses ./' + TARGET_TEMP + ' for output directory,\n' +
'runs cargo clean, runs cargo build -v, saves output to ./cargo.out,\n' +
'and writes to BUILD.gn 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')
# Rust package name with suffix -d1.d2.d3(+.*)?.
VERSION_SUFFIX_RE = re.compile(r'^(.*)-[0-9]+\.[0-9]+\.[0-9]+(?:\+.*)?$')
# Crate types corresponding to a library
LIBRARY_CRATE_TYPES = ['staticlib', 'cdylib', 'lib', 'rlib', 'dylib', 'proc-macro']
def escape_quotes(s):
# replace '"' with '\\"'
return s.replace('"', '\\"')
def file_base_name(path):
return os.path.splitext(os.path.basename(path))[0]
def pkg_to_crate_name(s):
return s.replace('-', '_').replace('.', '_')
def get_base_name(path):
return pkg_to_crate_name(file_base_name(path))
def get_crate_name(crate):
# to sort crates in a list
return crate.crate_name
def get_designated_pkg_info(lines, designated):
package = re.compile(r'^ *\[package\]')
designated_re = re.compile('^ *' + designated + ' *= * "([^"]*)')
is_package = False
for line in lines:
if is_package:
if designated_re.match(line):
line = eval(repr(line).replace(f'\\"', ''))
return designated_re.match(line).group(1)
else:
is_package = package.match(line) is not None
return ''
def is_build_script(name):
# Judge whether it is build script.
return name.startswith('build_script_')
def is_dependent_path(path):
# Absolute('/') or dependent('.../') paths are not main files of this crate.
return path.startswith('/') or path.startswith('.../')
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_RE.match(s):
return VERSION_SUFFIX_RE.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)
class Crate(object):
"""Information of a Rust crate to collect/emit for an BUILD.gn module."""
def __init__(self, runner, outfile_name):
# Remembered global runner
self.runner = runner
self.debug = runner.args.debug
self.cargo_dir = '' # directory of my Cargo.toml
self.outfile = None # open file handle of outfile_name during dump*
self.outfile_name = outfile_name # path to BUILD.gn
# GN module properties derived from rustc parameters.
self.module_type = '' # lib,crate_name,test etc.
self.root_pkg_name = '' # parent package name of a sub/test packge
# Save parsed status
self.error_infos = '' # all errors found during parsing
self.line = '' # original rustc command line parameters
self.line_num = 1 # runner told input source line number
# Parameters collected from rustc command line.
self.cap_lints = ''
self.crate_name = ''
self.edition = '2015' # cargo default is 2015, you can specify the edition as 2018 or 2021
self.emit_list = '' # --emit=dep-info,metadata,link
self.main_src = ''
self.target = ''
self.cfgs = list()
self.core_deps = list() # first part of self.deps elements
self.crate_types = list()
self.deps = list()
self.features = list()
self.ignore_options = list()
self.srcs = list() # main_src or merged multiple source files
self.shared_libs = list() # -l dylib=wayland-client, -l z
self.static_libs = list() # -l static=host_cpuid
# Parameters collected from Cargo.toml.
self.cargo_pkg_version = '' # value extracted from Cargo.toml version field
self.cargo_pkg_authors = '' # value extracted from Cargo.toml authors field
self.cargo_pkg_name = '' # value extracted from Cargo.toml name field
self.cargo_pkg_description = '' # value extracted from Cargo.toml description field
# Parameters related to build.rs.
self.build_root = ''
self.checked_out_files = False # to check only once
self.build_script_outputs = [] # output files generated by build.rs
def write(self, s):
# convenient way to output one line at a time with EOL.
self.outfile.write(s + '\n')
def parse_rustc(self, line_num, line):
"""Find important rustc arguments to convert to BUILD.gn properties."""
self.line_num = line_num
self.line = line
args = line.split() # Loop through every argument of rustc.
self.parse_args(args)
if not self.crate_name:
self.error_infos += 'ERROR: missing --crate-name\n'
if not self.crate_types:
if 'test' in self.cfgs:
self.crate_types.append('test')
else:
self.error_infos += 'ERROR: missing --crate-type or --test\n'
elif len(self.crate_types) > 1:
if 'lib' in self.crate_types and 'rlib' in self.crate_types:
self.error_infos += 'ERROR: cannot generate both lib and rlib crate types\n'
if 'test' in self.crate_types:
self.error_infos += 'ERROR: cannot handle both --crate-type and --test\n'
if not self.main_src:
self.error_infos += 'ERROR: missing main source file\n'
else:
self.srcs.append(self.main_src)
if self.cargo_dir:
self.get_root_pkg_name()
if not self.root_pkg_name:
self.root_pkg_name = self.crate_name
# Process crate with build.rs
if not self.skip_crate():
if not self.runner.args.no_pkg_info:
self.find_pkg_info()
self.find_build_root()
if self.runner.args.copy_out:
self.copy_out_files()
elif self.find_out_files() and self.has_used_out_dir():
self.copy_out_files()
self.cfgs = sorted(set(self.cfgs))
self.core_deps = sorted(set(self.core_deps))
self.crate_types = sorted(set(self.crate_types))
self.deps = sorted(set(self.deps))
self.features = sorted(set(self.features))
self.ignore_options = sorted(set(self.ignore_options))
self.static_libs = sorted(set(self.static_libs))
self.shared_libs = sorted(set(self.shared_libs))
self.decide_module_type()
return self
def parse_args(self, args):
num = 0
while num < len(args):
arg = args[num]
if arg == '--crate-name':
num += 1
self.crate_name = args[num]
elif arg == '--crate-type':
num += 1
self.crate_types.append(args[num])
elif arg == '--cfg':
num += 1
self.deal_cfg(args[num])
elif arg == '-C':
num += 1
self.add_ignore_options_flag(args[num]) # codegen options
elif arg.startswith('-C'):
self.add_ignore_options_flag(arg[2:])
elif arg == '--cap-lints':
num += 1
self.cap_lints = args[num]
elif arg.startswith('--edition='):
self.edition = arg.replace('--edition=', '')
elif arg.startswith('--emit='):
self.emit_list = arg.replace('--emit=', '')
elif arg == '--extern':
num += 1
self.deal_extern(args[num])
elif (arg.startswith('--error-format=') or arg.startswith('--json=') or
arg.startswith('\'-Aclippy')):
_ = arg # ignored
elif arg == '-L':
num += 1
self.set_root_pkg_name(args[num])
elif arg == '-l':
num += 1
self.deal_static_and_dylib(args[num])
elif arg == '--out-dir' or arg == '--color': # ignored
num += 1
elif arg == '--target':
num += 1
self.target = args[num]
elif arg == '--test':
self.crate_types.append('test')
elif not arg.startswith('-'):
self.set_main_src(args[num])
else:
self.error_infos += 'ERROR: unknown ' + arg + '\n'
num += 1
def deal_cfg(self, arg):
if arg.startswith('\'feature='):
feature = unquote(arg.replace('\'feature=', '')[:-1])
# 'runtime' feature removed because it conflicts with static
if feature == 'runtime':
feature = 'static'
self.features.append(feature)
else:
self.cfgs.append(arg)
def add_ignore_options_flag(self, flag):
"""Ignore options not used in GN."""
# 'codegen-units' is set in GN global config or by default
# 'embed-bitcode' is ignored; we might control LTO with other .gn flag
# 'prefer-dynamic' does not work with common flag -C lto
if not (flag.startswith('codegen-units=') or flag.startswith('debuginfo=') or
flag.startswith('embed-bitcode=') or flag.startswith('extra-filename=') or
flag.startswith('incremental=') or flag.startswith('metadata=') or
flag == 'prefer-dynamic'):
self.ignore_options.append(flag)
def deal_extern(self, arg):
deps = re.sub('=/[^ ]*/deps/', ' = ', arg)
self.deps.append(deps)
self.core_deps.append(re.sub(' = .*', '', deps))
def set_root_pkg_name(self, arg):
if arg.startswith('dependency=') and arg.endswith('/deps'):
if '/' + TARGET_TEMP + '/' in arg:
self.root_pkg_name = re.sub('^.*/', '',
re.sub('/' + TARGET_TEMP + '/.*/deps$', '', arg))
else:
self.root_pkg_name = re.sub('^.*/', '',
re.sub('/[^/]+/[^/]+/deps$', '', arg))
self.root_pkg_name = remove_version_suffix(self.root_pkg_name)
def deal_static_and_dylib(self, arg):
if arg.startswith('static='):
self.static_libs.append(re.sub('static=', '', arg))
elif arg.startswith('dylib='):
self.shared_libs.append(re.sub('dylib=', '', arg))
else:
self.shared_libs.append(arg)
def set_main_src(self, arg):
self.main_src = re.sub(r'^/[^ ]*/registry/src/', '.../', arg)
self.main_src = re.sub(r'^\.\.\./github.com-[0-9a-f]*/', '.../', self.main_src)
self.find_cargo_dir()
if self.cargo_dir:
if self.runner.args.no_subdir:
# all .gn content to /dev/null
self.outfile_name = '/dev/null'
elif not self.runner.args.one_file:
# Use Cargo.toml to write BUILD.gn in the subdirectory.
self.outfile_name = os.path.join(self.cargo_dir, 'BUILD.gn')
self.main_src = self.main_src[len(self.cargo_dir) + 1:]
def find_cargo_dir(self):
"""Deepest directory with Cargo.toml and contains the main_src."""
if not is_dependent_path(self.main_src):
dir_name = os.path.dirname(self.main_src)
while dir_name:
if dir_name.endswith('.'):
dir_name = os.path.dirname(dir_name)
continue
if os.path.exists(os.path.join(dir_name, 'Cargo.toml')):
self.cargo_dir = dir_name
return
dir_name = os.path.dirname(dir_name)
def skip_crate(self):
"""Return crate_name or a message if this crate should be skipped."""
# Some Rust packages include extra unwanted crates.
# This set contains all such excluded crate names.
excluded_crates = set(['protobuf_bin_gen_rust_do_not_use'])
if (is_build_script(self.crate_name) or
self.crate_name in excluded_crates):
return self.crate_name
if is_dependent_path(self.main_src):
return 'dependent crate'
return ''
def get_root_pkg_name(self):
"""Read name of [package] in ./Cargo.toml."""
cargo_toml_path = './Cargo.toml'
if self.cargo_dir:
cargo_toml_path = os.path.join(
os.path.join('.', self.cargo_dir), 'Cargo.toml')
if not os.path.exists(cargo_toml_path):
return
with open(cargo_toml_path, 'r') as infile:
self.root_pkg_name = get_designated_pkg_info(infile, 'name')
return
def find_pkg_info(self):
"""Read package info of [package] in ./Cargo.toml."""
cargo_toml_path = './Cargo.toml'
if self.cargo_dir:
cargo_toml_path = os.path.join(
os.path.join('.', self.cargo_dir), 'Cargo.toml')
if not os.path.exists(cargo_toml_path):
return
with open(cargo_toml_path, 'r') as infile:
if self.root_pkg_name:
self.cargo_pkg_name = self.root_pkg_name
else:
self.cargo_pkg_name = get_designated_pkg_info(infile, 'name')
infile.seek(0)
self.cargo_pkg_version = get_designated_pkg_info(infile, 'version')
infile.seek(0)
pkg_description = get_designated_pkg_info(infile, 'description')
pkg_description = pkg_description.replace('\n', '').replace(r'\n', '').strip()
self.cargo_pkg_description = pkg_description
infile.seek(0)
authors_re = re.compile(' *authors *= * \[(.*?)\]', re.S)
authors_section = authors_re.search(infile.read())
if authors_section:
authors = authors_section.group(1)
authors = authors.replace('\n', '').replace(' ', ' ').replace('"', '').strip()
if authors.endswith(','):
authors = authors[:-1]
self.cargo_pkg_authors = authors
def find_build_root(self):
"""Read build of [package] in ./Cargo.toml."""
cargo_toml_path = './Cargo.toml'
if self.cargo_dir:
cargo_toml_path = os.path.join(
os.path.join('.', self.cargo_dir), 'Cargo.toml')
if not os.path.exists(cargo_toml_path):
return
with open(cargo_toml_path, 'r') as infile:
self.build_root = get_designated_pkg_info(infile, 'build')
if not self.build_root:
build_rs_path = './build.rs'
if self.cargo_dir:
build_rs_path = os.path.join(os.path.join('.', self.cargo_dir), 'build.rs')
if os.path.exists(build_rs_path):
self.build_root = 'build.rs'
def find_out_files(self):
# normal_output_list has build.rs output for normal crates
normal_output_list = glob.glob(
TARGET_TEMP + '/*/*/build/' + self.root_pkg_name + '-*/out/*')
# other_output_list has build.rs output for proc-macro crates
other_output_list = glob.glob(
TARGET_TEMP + '/*/build/' + self.root_pkg_name + '-*/out/*')
return normal_output_list + other_output_list
def has_used_out_dir(self):
"""Returns true if env!("OUT_DIR") is found."""
cmd = 'grep -rl --exclude build.rs --include \\*.rs \'env!("OUT_DIR")\' * > /dev/null'
if self.cargo_dir:
cmd = 'grep -rl --exclude '
cmd += os.path.join(self.cargo_dir, 'build.rs')
cmd += ' --include \\*.rs \'env!("OUT_DIR")\' * > /dev/null'
return 0 == os.system(cmd)
def copy_out_files(self):
"""Copy build.rs output files to ./out and set up build_script_outputs."""
if self.checked_out_files:
return
self.checked_out_files = True
cargo_out_files = self.find_out_files()
out_files = set()
out_path = 'out'
if self.cargo_dir:
out_path = os.path.join(self.cargo_dir, out_path)
if cargo_out_files:
os.makedirs(out_path, exist_ok=True)
for path in cargo_out_files:
file_name = path.split('/')[-1]
out_files.add(file_name)
self.build_script_outputs = sorted(out_files)
def decide_module_type(self):
# Use the first crate type for the default/first module.
crate_type = self.crate_types[0] if self.crate_types else ''
self.decide_one_module_type(crate_type)
def decide_one_module_type(self, crate_type):
"""Decide which GN module type to use."""
if crate_type == 'bin':
self.module_type = self.crate_name
elif crate_type in LIBRARY_CRATE_TYPES:
self.module_type = 'lib'
elif crate_type == 'test':
self.module_type = 'test'
else:
self.module_type = ''
def merge_crate(self, other, outfile_name):
"""Try to merge crate into self."""
# Cargo build --tests could recompile a library for tests.
# We need to merge such duplicated calls to rustc, with the
# algorithm in is_should_merge.
should_merge = self.is_should_merge(other)
should_merge_test = False
if not should_merge:
should_merge_test = self.merge_test(other)
if should_merge or should_merge_test:
self.runner.init_gn_file(outfile_name)
# to write debug info
with open(outfile_name, 'a') as outfile:
self.outfile = outfile
other.outfile = outfile
self.execute_merge(other, should_merge_test)
return True
return False
def is_should_merge(self, other):
return (self.crate_name == other.crate_name and
self.crate_types == other.crate_types and
self.main_src == other.main_src and
self.root_pkg_name == other.root_pkg_name and
not self.skip_crate() and self.is_same_flags(other))
def merge_test(self, other):
"""Returns true if self and other are tests of same root_pkg_name."""
# 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_types == other.crate_types and self.crate_types == ['test'] and
self.root_pkg_name == other.root_pkg_name and not self.skip_crate() and
other.crate_name == get_base_name(other.main_src) and
(len(self.srcs) > 1 or (self.crate_name == get_base_name(self.main_src))) and
self.is_same_flags(other))
def is_same_flags(self, other):
return (not self.error_infos and not other.error_infos and
self.cap_lints == other.cap_lints and self.cfgs == other.cfgs and
self.core_deps == other.core_deps and self.edition == other.edition and
self.emit_list == other.emit_list and self.features == other.features and
self.ignore_options == other.ignore_options and
self.static_libs == other.static_libs and
self.shared_libs == other.shared_libs)
def execute_merge(self, other, should_merge_test):
"""Merge attributes of other to self."""
if self.debug:
self.write('\n// Before merge definition(self):')
self.dump_debug_info()
self.write('\n// Before merge definition(other):')
other.dump_debug_info()
if not self.target:
# okay to keep only the first target triple
self.target = other.target
self.decide_module_type()
if should_merge_test:
if (self.runner.should_ignore_test(self.main_src) and
not self.runner.should_ignore_test(other.main_src)):
self.main_src = other.main_src
self.srcs.append(other.main_src)
self.crate_name = pkg_to_crate_name(self.root_pkg_name)
if self.debug:
self.write('\n// After merge definition:')
self.dump_debug_info()
def dump(self):
"""Dump all error/debug/module code to the output .gn file."""
self.runner.init_gn_file(self.outfile_name)
with open(self.outfile_name, 'a') as outfile:
self.outfile = outfile
if self.error_infos:
self.dump_line()
self.write(self.error_infos)
elif self.skip_crate():
self.dump_skip_crate(self.skip_crate())
else:
if self.debug:
self.dump_debug_info()
self.dump_gn_module()
def dump_debug_info(self):
"""Dump parsed data, when cargo2gn is called with --debug."""
def dump(name, value):
self.write('//%12s = %s' % (name, value))
def dump_list(fmt, values):
for v in values:
self.write(fmt % v)
def opt_dump(name, value):
if value:
dump(name, value)
self.dump_line()
dump('crate_name', self.crate_name)
dump('crate_types', self.crate_types)
opt_dump('edition', self.edition)
opt_dump('emit_list', self.emit_list)
dump('main_src', self.main_src)
dump('module_type', self.module_type)
opt_dump('target', self.target)
opt_dump('cap_lints', self.cap_lints)
dump_list('// cfg = %s', self.cfgs)
dump_list('// cfg = \'feature "%s"\'', self.features)
dump_list('// codegen = %s', self.ignore_options)
dump_list('// deps = %s', self.deps)
dump_list('// -l (dylib) = %s', self.shared_libs)
dump_list('// -l static = %s', self.static_libs)
def dump_line(self):
self.write('\n// Line ' + str(self.line_num) + ' ' + self.line)
def dump_skip_crate(self, kind):
if self.debug:
self.write('\n// IGNORED: ' + kind + ' ' + self.main_src)
return self
def dump_gn_module(self):
"""Dump one or more GN module definition, depending on crate_types."""
if len(self.crate_types) == 1:
self.dump_single_type_gn_module()
return
if 'test' in self.crate_types:
self.write('\nERROR: multiple crate types cannot include test type')
return
# Dump one GN module per crate_type.
for crate_type in self.crate_types:
self.decide_one_module_type(crate_type)
self.dump_one_gn_module(crate_type)
def dump_single_type_gn_module(self):
"""Dump one simple GN module, which has only one crate_type."""
crate_type = self.crate_types[0]
if crate_type != 'test':
self.dump_one_gn_module(crate_type)
return
# Dump one test module per source file.
self.srcs = [
src for src in self.srcs if not self.runner.should_ignore_test(src)]
if len(self.srcs) > 1:
self.srcs = sorted(set(self.srcs))
saved_srcs = self.srcs
for src in saved_srcs:
self.srcs = [src]
saved_main_src = self.main_src
self.main_src = src
self.decide_one_module_type(crate_type)
self.dump_one_gn_module(crate_type)
self.main_src = saved_main_src
self.srcs = saved_srcs
def dump_one_gn_module(self, crate_type):
"""Dump one GN module definition."""
if not self.module_type:
self.write('\nERROR: unknown crate_type ' + crate_type)
return
self.write('\nohos_cargo_crate("' + self.module_type + '") {')
self.dump_gn_first_properties(crate_type)
self.dump_gn_core_properties()
self.write('}')
def dump_gn_first_properties(self, crate_type):
if crate_type != 'bin':
self.write(' crate_name = "' + self.crate_name + '"')
if crate_type:
if crate_type == 'lib':
crate_type = 'rlib'
self.write(' crate_type = "' + crate_type + '"')
if self.main_src:
self.write(' crate_root = "' + self.main_src + '"')
if self.crate_name.startswith('lib'):
self.write(' output_name = "lib' + self.crate_name + '"')
self.write('')
def dump_gn_core_properties(self):
self.dump_sources_list()
if self.edition:
self.write(' edition = "' + self.edition + '"')
if not self.runner.args.no_pkg_info:
if self.cargo_pkg_version:
self.write(' cargo_pkg_version = "' +
self.cargo_pkg_version + '"')
if self.cargo_pkg_authors:
self.write(' cargo_pkg_authors = "' +
self.cargo_pkg_authors + '"')
if self.cargo_pkg_name:
self.write(' cargo_pkg_name = "' +
self.cargo_pkg_name + '"')
if self.cargo_pkg_description:
self.write(' cargo_pkg_description = "' +
self.cargo_pkg_description + '"')
if self.deps:
self.dump_gn_deps()
if self.build_root and self.root_pkg_name in self.runner.build_deps:
self.dump_gn_build_deps()
self.dump_gn_property_list('features', '"%s"', self.features)
if self.build_root:
self.write(' build_root = "' + self.build_root + '"')
build_sources = list()
build_sources.append(self.build_root)
self.dump_gn_property_list('build_sources', '"%s"', build_sources)
if self.build_script_outputs:
self.dump_gn_property_list(
'build_script_outputs', '"%s"', self.build_script_outputs)
def dump_sources_list(self):
"""Dump the srcs list, for defaults or regular modules."""
if len(self.srcs) > 1:
srcs = sorted(set(self.srcs)) # make a copy and dedup
for num in range(len(self.srcs)):
srcs[num] = srcs[num]
else:
srcs = [self.main_src]
self.dump_gn_property_list('sources', '"%s"', srcs)
def dump_gn_deps(self):
"""Dump the deps."""
rust_deps = list()
deps_libname = re.compile('^.* = lib(.*)-[0-9a-f]*.(rlib|so|rmeta)$')
for lib in self.deps:
libname_groups = deps_libname.match(lib)
if libname_groups is not None:
lib_name = libname_groups.group(1)
else:
lib_name = re.sub(' .*$', '', lib)
if lib_name in self.runner.args.dependency_blocklist:
continue
if lib.endswith('.rlib') or lib.endswith('.rmeta') or lib.endswith('.so'):
# On MacOS .rmeta is used when Linux uses .rlib or .rmeta.
rust_lib = self.get_rust_lib(lib_name)
if rust_lib:
rust_lib += ':lib'
rust_deps.append(rust_lib)
elif lib != 'proc_macro':
# --extern proc_macro is special and ignored
rust_deps.append('// unknown type of lib: '.join(lib))
if rust_deps:
self.dump_gn_property_list('deps', '"%s"', rust_deps)
def dump_gn_build_deps(self):
"""Dump the build deps."""
rust_build_deps = list()
build_deps_libname = re.compile('^.* = lib(.*)-[0-9a-f]*.(rlib|so|rmeta)$')
build_deps = self.runner.build_deps.get(self.root_pkg_name)
if not build_deps:
return
for lib in build_deps:
libname_groups = build_deps_libname.match(lib)
if libname_groups is not None:
lib_name = libname_groups.group(1)
else:
lib_name = re.sub(' .*$', '', lib)
if lib_name in self.runner.args.dependency_blocklist:
continue
if lib.endswith('.rlib') or lib.endswith('.rmeta') or lib.endswith('.so'):
# On MacOS .rmeta is used when Linux uses .rlib or .rmeta.
rust_lib = self.get_rust_lib(lib_name)
if rust_lib:
rust_build_deps.append(rust_lib + ':lib')
elif lib != 'proc_macro':
# --extern proc_macro is special and ignored
rust_build_deps.append('// unknown type of lib: '.join(lib))
if rust_build_deps:
self.dump_gn_property_list('build_deps', '"%s"', rust_build_deps)
def dump_gn_property_list(self, name, fmt, values):
if not values:
return
if len(values) > 1:
self.write(' ' + name + ' = [')
self.dump_gn_property_list_items(fmt, values)
self.write(' ]')
else:
self.write(' ' + name + ' = [' +
(fmt % escape_quotes(values[0])) + ']')
def dump_gn_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 get_rust_lib(self, lib_name):
rust_lib = ''
if lib_name:
crate_name = pkg_to_crate_name(lib_name)
deps_libname = self.runner.deps_libname_map.get(crate_name)
if deps_libname:
rust_lib = RUST_PATH + deps_libname
return rust_lib
class Runner(object):
"""Main class to parse cargo -v output"""
def __init__(self, args):
self.gn_files = set() # Remember all output BUILD.gn files.
self.root_pkg_name = '' # name of package in ./Cargo.toml
self.args = args
self.dry_run = not args.run
self.skip_cargo = args.skipcargo
self.cargo_path = './cargo' # path to cargo
self.crates = list() # all crates
self.error_infos = '' # all error infos
self.test_error_infos = '' # all test error infos
self.warning_files = set() # all warning files
self.set_cargo_path()
# Default operation is cargo clean, followed by build or user given operation.
if args.cargo:
self.cargo = ['clean'] + args.cargo
else:
# Use the same target for both host and default device builds.
self.cargo = ['clean', 'build --target x86_64-unknown-linux-gnu']
self.empty_tests = set()
self.empty_unittests = False
self.build_deps = {}
self.deps_libname_map = {}
def set_cargo_path(self):
"""Find cargo in the --cargo_bin and set cargo path"""
if self.args.cargo_bin:
self.cargo_path = os.path.join(self.args.cargo_bin, 'cargo')
if os.path.isfile(self.cargo_path):
print('INFO: using cargo in ' + self.args.cargo_bin)
return
else:
sys.exit('ERROR: cannot find cargo in ' + self.args.cargo_bin)
else:
sys.exit('ERROR: the prebuilt cargo is not available; please use the --cargo_bin flag.')
return
def run_cargo(self):
"""Run cargo -v and save its output to ./cargo.out."""
if self.skip_cargo:
return self
cargo_toml = './Cargo.toml'
cargo_out = './cargo.out'
if not os.access(cargo_toml, os.R_OK):
print('ERROR: Cannot find ', cargo_toml)
return self
cargo_lock = './Cargo.lock'
cargo_lock_save = './cargo.lock.save'
have_cargo_lock = os.path.exists(cargo_lock)
if not self.dry_run:
if os.path.exists(cargo_out):
os.remove(cargo_out)
if not self.args.use_cargo_lock and have_cargo_lock:
os.rename(cargo_lock, cargo_lock_save)
# set up search PATH for cargo to find the correct rustc
save_path = os.environ['PATH']
os.environ['PATH'] = os.path.dirname(self.cargo_path) + ':' + save_path
# Add [workspace] to Cargo.toml if it is non-existent.
is_add_workspace = False
if self.args.add_workspace:
with open(cargo_toml, 'r') as in_file:
cargo_toml_lines = in_file.readlines()
if '[workspace]\n' in cargo_toml_lines:
print('WARNING: found [workspace] in Cargo.toml')
else:
with open(cargo_toml, 'w') as out_file:
out_file.write('[workspace]\n')
is_add_workspace = True
self.deal_cargo_cmd(cargo_out)
# restore original Cargo.toml
if is_add_workspace:
with open(cargo_toml, 'w') as out_file:
out_file.writelines(cargo_toml_lines)
if not self.dry_run:
if not have_cargo_lock: # restore to no Cargo.lock state
if os.path.exists(cargo_lock):
os.remove(cargo_lock)
elif not self.args.use_cargo_lock: # restore saved Cargo.lock
os.rename(cargo_lock_save, cargo_lock)
os.environ['PATH'] = save_path
return self
def deal_cargo_cmd(self, cargo_out):
cargo_cmd_v_flag = ' -vv ' if self.args.vv else ' -v '
cargo_cmd_target_dir = ' --target-dir ' + TARGET_TEMP
cargo_cmd_redir = ' >> ' + cargo_out + ' 2>&1'
for cargo in self.cargo:
cargo_cmd = self.cargo_path + cargo_cmd_v_flag
features = ''
if cargo != 'clean':
if self.args.features is not None:
features = ' --no-default-features'
if self.args.features:
features += ' --features ' + self.args.features
cargo_cmd += cargo + features + cargo_cmd_target_dir + cargo_cmd_redir
if self.args.rustflags and cargo != 'clean':
cargo_cmd = 'RUSTFLAGS="' + self.args.rustflags + '" ' + cargo_cmd
self.run_cargo_cmd(cargo_cmd, cargo_out)
def run_cargo_cmd(self, cargo_cmd, cargo_out):
if self.dry_run:
print('Dry-run skip:', cargo_cmd)
else:
with open(cargo_out, 'a') as file:
file.write('### Running: ' + cargo_cmd + '\n')
ret = os.system(cargo_cmd)
if ret != 0:
print('ERROR: There was an error while running cargo.' +
' See the cargo.out file for details.')
def generate_gn(self):
"""Parse cargo.out and generate BUILD.gn files."""
cargo_out = 'cargo.out' # The file name used to save cargo build -v output.
errors_line = 'Errors in ' + cargo_out + ':'
if self.dry_run:
print('Dry-run skip: read', cargo_out, 'write BUILD.gn')
elif os.path.exists(cargo_out):
self.find_root_pkg()
with open(cargo_out, 'r') as cargo_out:
self.parse(cargo_out, 'BUILD.gn')
self.crates.sort(key=get_crate_name)
for crate in self.crates:
crate.dump()
if self.error_infos:
self.append_to_gn('\n' + errors_line + '\n' + self.error_infos)
if self.test_error_infos:
self.append_to_gn('\n// Errors when listing tests:\n' +
self.test_error_infos)
return self
def find_root_pkg(self):
"""Read name of [package] in ./Cargo.toml."""
if os.path.exists('./Cargo.toml'):
return
with open('./Cargo.toml', 'r') as infile:
get_designated_pkg_info(infile, 'name')
def parse(self, infile, outfile_name):
"""Parse rustc, test, and warning messages in infile, return a list of Crates."""
# cargo test --list output of the start of running a binary.
cargo_test_list_start_re = re.compile('^\s*Running (.*) \(.*\)$')
# cargo test --list output of the end of running a binary.
cargo_test_list_end_re = re.compile('^(\d+) tests, (\d+) benchmarks$')
compiling_pat = re.compile('^ +Compiling (.*)$')
current_test_name = None
for line in infile:
# We read the file in two passes, where the first simply checks for empty tests.
# Otherwise we would add and merge tests before seeing they're empty.
if cargo_test_list_start_re.match(line):
current_test_name = cargo_test_list_start_re.match(line).group(1)
elif current_test_name and cargo_test_list_end_re.match(line):
match = cargo_test_list_end_re.match(line)
if int(match.group(1)) + int(match.group(2)) == 0:
self.add_empty_test(current_test_name)
current_test_name = None
#Get Compiling information
if compiling_pat.match(line):
self.add_deps_libname_map(compiling_pat.match(line).group(1))
infile.seek(0)
self.parse_cargo_out(infile, outfile_name)
def add_empty_test(self, name):
if name == 'unittests':
self.empty_unittests = True
else:
self.empty_tests.add(name)
def add_deps_libname_map(self, line):
line_list = line.split()
if len(line_list) > 1:
self.deps_libname_map[pkg_to_crate_name(line_list[0])] = line_list[0]
def parse_cargo_out(self, infile, outfile_name):
# Cargo -v output of a call to rustc.
rustc_re = 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_re = re.compile('^ +Running `.*CARGO_.*=.*$')
# Rustc output of file location path pattern for a warning message.
warning_output_file_re = re.compile('^ *--> ([^:]*):[0-9]+')
cargo_to_gn_running_re = re.compile('^### Running: .*$')
line_num = 0
previous_warning = False # true if the previous line was warning
rustc_line = '' # previous line matching rustc_vv_re
in_tests = False
for line in infile:
line_num += 1
if line.startswith('warning: '):
previous_warning = True
rustc_line = self.assert_empty_rustc_line(rustc_line)
continue
new_rustc_line = ''
if rustc_re.match(line):
args_line = rustc_re.match(line).group(1)
self.add_crate(Crate(self, outfile_name).parse_rustc(line_num, args_line))
self.assert_empty_rustc_line(rustc_line)
elif rustc_line or rustc_vv_re.match(line):
new_rustc_line = self.deal_rustc_command(
line_num, rustc_line, line, outfile_name)
elif previous_warning and warning_output_file_re.match(line):
file_path = warning_output_file_re.match(line).group(1)
if file_path[0] != '/': # ignore absolute path
self.warning_files.add(file_path)
self.assert_empty_rustc_line(rustc_line)
elif line.startswith('error: ') or line.startswith('error[E'):
if not self.args.ignore_cargo_errors:
self.add_error_infos(in_tests, line)
elif cargo_to_gn_running_re.match(line):
in_tests = "cargo test" in line and "--list" in line
previous_warning = False
rustc_line = new_rustc_line
def assert_empty_rustc_line(self, line):
# report error if line is not empty
if line:
self.append_to_gn('ERROR -vv line: ' + line)
return ''
def append_to_gn(self, line):
self.init_gn_file('BUILD.gn')
with open('BUILD.gn', 'a') as outfile:
outfile.write(line)
print(line)
def init_gn_file(self, name):
# name could be BUILD.gn or sub_dir_path/BUILD.gn
if name in self.gn_files:
return
self.gn_files.add(name)
if os.path.exists(name):
os.remove(name)
with open(name, 'w') as outfile:
outfile.write(BUILD_GN_HEADER)
outfile.write('\n')
outfile.write('import("%s")\n' % IMPORT_CONTENT)
def add_error_infos(self, in_tests, line):
if in_tests:
self.test_error_infos += '// '.join(line)
else:
self.error_infos += line
def deal_rustc_command(self, line_num, rustc_line, line, outfile_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_line = (rustc_line.strip() + line) if rustc_line else line
# The combined -vv output rustc command line pattern.
rustc_vv_cmd_args = re.compile('^ *Running `.*CARGO_.*=.* rustc (.*)`$')
if not line.endswith('`\n') or (new_rustc_line.count('`') % 2) != 0:
return new_rustc_line
if rustc_vv_cmd_args.match(new_rustc_line):
args = rustc_vv_cmd_args.match(new_rustc_line).group(1)
self.add_crate(Crate(self, outfile_name).parse_rustc(line_num, args))
else:
self.assert_empty_rustc_line(new_rustc_line)
return ''
def add_crate(self, new_crate):
"""Merge crate with someone in crates, or append to it. Return crates."""
if (is_build_script(new_crate.crate_name) and
not is_dependent_path(new_crate.main_src) and
new_crate.root_pkg_name and len(new_crate.deps) > 0):
self.build_deps[new_crate.root_pkg_name] = new_crate.deps
if new_crate.skip_crate():
# include debug info of all crates
if self.args.debug:
self.crates.append(new_crate)
else:
for crate in self.crates:
if crate.merge_crate(new_crate, 'BUILD.gn'):
return
# If not merged, decide module type and name now.
new_crate.decide_module_type()
self.crates.append(new_crate)
def should_ignore_test(self, src):
# cargo test outputs the source file for integration tests but "unittests" for unit tests.
# To figure out to which crate this corresponds, we check if the current source file is
# the main source of a non-test crate, e.g., a library or a binary.
return (src in self.empty_tests or src in self.args.test_blocklist or
(self.empty_unittests and
src in [c.main_src for c in self.crates if c.crate_types != ['test']]))
def get_arg_parser():
"""Parse main arguments."""
argparser = argparse.ArgumentParser('cargo2gn')
argparser.add_argument('--add-workspace', action='store_true', default=False,
help=('append [workspace] to Cargo.toml before calling cargo, to treat' +
' current directory as root of package source; otherwise the relative' +
' source file path in generated .gn file will be from the parent directory.'))
argparser.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'))
argparser.add_argument('--cargo-bin', type=str,
help='use cargo in the cargo_bin directory instead of the prebuilt one')
argparser.add_argument('--config', type=str,
help=('Load command-line options from the given config file. ' +
'Options in this file will override those passed on the command line.'))
argparser.add_argument('--copy-out', action='store_true', default=False,
help=('only for root directory, copy build.rs output to ./out/* and ' +
'add a genrule to copy ./out/*.'))
argparser.add_argument('--debug', action='store_true', default=False,
help='dump debug info into BUILD.gn')
argparser.add_argument('--dependency-blocklist', nargs='*', default=[],
help='Do not emit the given dependencies (without lib prefixes).')
argparser.add_argument('--features', type=str,
help=('pass features to cargo build, ' +
'empty string means no default features'))
argparser.add_argument('--ignore-cargo-errors', action='store_true', default=False,
help='do not append cargo/rustc error messages to BUILD.gn')
argparser.add_argument('--no-pkg-info', action='store_true', default=False,
help='Do not attempt to determine the package info automatically.')
argparser.add_argument('--no-subdir', action='store_true', default=False,
help='do not output anything for sub-directories')
argparser.add_argument('--one-file', action='store_true', default=False,
help=('output all into one BUILD.gn, default will generate one BUILD.gn ' +
'per Cargo.toml in subdirectories'))
argparser.add_argument('--run', action='store_true', default=False,
help='run it, default is dry-run')
argparser.add_argument('--rustflags', type=str, help='passing flags to rustc')
argparser.add_argument('--skipcargo', action='store_true', default=False,
help='skip cargo command, parse cargo.out, and generate BUILD.gn')
argparser.add_argument('--test-blocklist', nargs='*', default=[],
help=('Do not emit the given tests. ' +
'Pass the path to the test file to exclude.'))
argparser.add_argument('--use-cargo-lock', action='store_true', default=False,
help=('run cargo build with existing Cargo.lock ' +
'(used when some latest dependent crates failed)'))
argparser.add_argument('--vv', action='store_true', default=False,
help='run cargo with -vv instead of default -v')
return argparser
def get_parse_args(argparser):
"""Parses command-line options."""
args = argparser.parse_args()
# Use the values specified in a config file if one was found.
if args.config:
with open(args.config, 'r') as file:
config_data = json.load(file)
args_dict = vars(args)
for arg in config_data:
args_dict[arg.replace('-', '_')] = config_data[arg]
return args
def main():
argparser = get_arg_parser()
args = get_parse_args(argparser)
if not args.run: # default is dry-run
print(DRY_RUN_CONTENT)
Runner(args).run_cargo().generate_gn()
if __name__ == '__main__':
main()