blob: f4c0c2fa42b0af78cf0526b4fc8f1d8de59f7da7 [file] [log] [blame]
# Copyright 2022 The ChromiumOS Authors
# Use of this source code is governed by a BSD-style license that can be
# found in the LICENSE file.
"""Converts compilation database to work outside chroot.
Gets a compilation database generated inside chroot from stdin and outputs the
one that works outside chroot to stdout. This script is used from
platform.eclass.
"""
import json
import os
import re
import shutil
import subprocess
import sys
from typing import Callable, List
from chromite.ide_tooling.scripts import detect_indent
MNT_HOST_SOURCE_RE = r"(?:\.\.(?:/\.\.)*)?/mnt/host/source/(.*)"
def is_mount(directory: str) -> bool:
try:
subprocess.check_output(["mountpoint", directory])
return True
except subprocess.CalledProcessError:
return False
class Converter:
"""Converts compilation database to work outside chroot"""
def __init__(
self, external_trunk_path: str, which: Callable[[str], str]
) -> None:
self.external_trunk_path = external_trunk_path
self.external_chroot_path = os.path.join(external_trunk_path, "chroot")
self.external_out_path = os.path.join(external_trunk_path, "out")
self.build_is_mount = is_mount("/build")
self.which = which
def external_filepath(self, filepath: str) -> str:
if filepath.startswith("/"):
if filepath.startswith("/build/") and self.build_is_mount:
return os.path.join(self.external_out_path, filepath[1:])
return os.path.join(self.external_chroot_path, filepath[1:])
return filepath
def convert_filepath(self, filepath: str) -> str:
# If out-of-tree build is enabled, source files under /mnt/host/source
# are used. This directory is inaccessible outside chroot, so we convert
# it.
m = re.fullmatch(MNT_HOST_SOURCE_RE, filepath)
if m:
return os.path.join(self.external_trunk_path, m[1])
# If out-of-tree build is disabled, source files are copied in a
# temporary directory, and the filepath may point to the copied file. We
# convert this so that code navigation doesn't jump to the file in
# chroot.
# We use a heuristic (using /platform2/ as a marker) here to support
# platform2 packages.
# TODO(oka): Revisit this logic to support packages outside platform2.
m = re.fullmatch(r".*/platform2(/.*)?", filepath)
if m:
platform2 = os.path.join(self.external_trunk_path, "src/platform2")
if m[1]:
return os.path.join(platform2, m[1][1:])
return platform2
return self.external_filepath(filepath)
def convert_include_option(self, option: str) -> List[str]:
"""Converts include option to work outside chroot.
Examples:
platform2 directory is converted, and the original one that is
invisible outside chroot is omitted.
.../mnt/host/source/src/platform2
-> /path/to/chromiumos/src/platform2
platform2 directory is converted, and the original one that is
visible outside chroot is also kept.
.../tmp/whatever/platform2
-> /path/to/chromiumos/src/platform2 .../tmp/whatever/platform2
Paths having no corresponding sources outside chroot are not
converted.
gen/include
-> gen/include
"""
filepath = option[2:]
converted_include = "-I" + self.convert_filepath(filepath)
# The filepath is not visible outside chroot.
if re.fullmatch(MNT_HOST_SOURCE_RE, filepath):
return [converted_include]
chroot_include = "-I" + self.external_filepath(filepath)
# chroot_include always points to the file inside chroot and might be
# different from converted_include.
if converted_include == chroot_include:
return [converted_include]
return [converted_include, chroot_include]
def convert_clang_option_value(self, value: str) -> str:
if "/" in value:
return self.convert_filepath(value)
return value
def convert_clang_option(self, option: str) -> List[str]:
if not option.startswith("-"):
return [self.convert_clang_option_value(option)]
# Convert flag value
if option.startswith("-I"):
return self.convert_include_option(option)
# Remove a few compiler options that might not be available in the
# potentially older clang version outside the chroot.
if option in [
"-fdebug-info-for-profiling",
"-mretpoline",
"-mretpoline-external-thunk",
"-mfentry",
"-mno-sched-prolog",
"-fconserve-stack",
]:
return []
if "=" in option:
flag, value = option.split("=", 1)
return [flag + "=" + self.convert_clang_option_value(value)]
if "/" in option:
raise Exception(f"Unknown flag that suffixes a filepath: {option}")
return [option]
def convert_exe(self, exe: str) -> str:
if exe == "cc":
return exe
# We should call clang directly instead of using CrOS wrappers.
if re.match(r"^(aarch64|arm)", exe):
return os.path.join(self.external_chroot_path, self.which(exe))
for x in ["clang", "clang++"]:
if exe.endswith(x):
return x
raise Exception(f"Unexpected executable name: {exe}")
def convert_command_list(self, command: List[str]) -> List[str]:
exe, *options = command
converted_exe = self.convert_exe(exe)
converted_options = []
for option in options:
converted_options += self.convert_clang_option(option)
# Add "-stdlib=libc++" so that the clang outside the chroot can
# find built-in headers like <string> and <memory>
if "clang" in converted_exe:
converted_options.append("-stdlib=libc++")
return [converted_exe, *converted_options]
def convert_command(self, command: str) -> str:
return " ".join(self.convert_command_list(command.split(" ")))
DIRECTORY = "directory"
COMMAND = "command"
FILE = "file"
OUTPUT = "output"
ARGUMENTS = "arguments"
def generate(
data, external_trunk_path, which: Callable[[str], str] = shutil.which
):
"""Generates non-chroot version of the compilation database"""
converter = Converter(external_trunk_path, which)
converted = []
for item in data:
converted_item = {}
if ARGUMENTS in item:
converted_item[ARGUMENTS] = converter.convert_command_list(
item[ARGUMENTS]
)
converted_item[DIRECTORY] = converter.convert_filepath(item[DIRECTORY])
if COMMAND in item:
converted_item[COMMAND] = converter.convert_command(item[COMMAND])
converted_item[FILE] = converter.convert_filepath(item[FILE])
if OUTPUT in item:
converted_item[OUTPUT] = converter.convert_filepath(item[OUTPUT])
converted.append(converted_item)
return converted
def main(argv: List[str]) -> None:
"""Main function to convert the compilation database.
Args:
argv: Command-line args passed into the script, i.e. sys.argv[1:].
"""
text = sys.stdin.read()
data = json.loads(text)
external_trunk_path = argv[0]
if not os.path.exists(external_trunk_path):
# The external_trunk_path points to the chromiumos trunk path *outside*
# chroot, and it may not exist inside chroot, where the script is run
# (b:259342928). We still show a warning here for debuggability because
# the path usually exists.
print(
f"{external_trunk_path} does not exist inside chroot",
file=sys.stderr,
)
indent = detect_indent.detect_indentation(text)
json.dump(generate(data, external_trunk_path), sys.stdout, indent=indent)