blob: 262fda5334c4c1b41cdf6584bade4af923413328 [file] [log] [blame]
// Copyright 2022 Google LLC
//
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
package exporter
import (
"bytes"
"fmt"
"path/filepath"
"strings"
"go.skia.org/infra/go/skerr"
"go.skia.org/skia/bazel/exporter/build_proto/analysis_v2"
"go.skia.org/skia/bazel/exporter/build_proto/build"
"go.skia.org/skia/bazel/exporter/interfaces"
"google.golang.org/protobuf/proto"
)
type CMakeExporter struct {
projName string
workspace cmakeWorkspace
workspaceDir string // Absolute path to Bazel workspace directory.
cmakeFile string // Absolute path to CMake output file.
fs interfaces.FileSystem
}
// NewCMakeExporter creates an exporter that will export a Bazel project
// query from a project in the workspaceDir to a CMake project file identified
// by cmakeFile.
//
// Note: cmakeFile must be an absolute path.
func NewCMakeExporter(projName, workspaceDir, cmakeFile string, fs interfaces.FileSystem) *CMakeExporter {
return &CMakeExporter{
workspace: *newCMakeWorkspace(),
projName: projName,
workspaceDir: workspaceDir,
cmakeFile: cmakeFile,
fs: fs,
}
}
// Return the default copts (COMPILE_FLAGS in CMake) for the macOS toolchain.
func getMacPlatformRuleCopts() []string {
// TODO(crbug.com/skia/13586): Retrieve these values from Bazel.
// These values must match those values defined in mac_toolchain_config.bzl
return []string{
// These items are from _make_default_flags().
"-std=c++17",
"-Wno-psabi",
// From _make_target_specific_flags.
"--target=arm64-apple-macos11",
}
}
// Return the default copts (COMPILE_FLAGS in CMake) for the Linux toolchain.
func getLinuxPlatformRuleCopts() []string {
// TODO(crbug.com/skia/13586): Retrieve these values from Bazel.
return []string{
// These items are from _make_default_flags().
"-std=c++17",
"-Wno-psabi",
// Added to avoid compile warning.
"-Wno-attributes",
}
}
// Write the CMake project config to set the COMPILE_FLAGS
// variables for all platforms.
func writePlatformCompileFlags(writer interfaces.Writer) {
val := strings.Join(getMacPlatformRuleCopts(), " ")
fmt.Fprintf(writer, "set(DEFAULT_COMPILE_FLAGS_MACOS %q)\n", val)
val = strings.Join(getLinuxPlatformRuleCopts(), " ")
fmt.Fprintf(writer, "set(DEFAULT_COMPILE_FLAGS_LINUX %q)\n", val)
writer.WriteString("\n")
fmt.Fprintln(writer, `if (APPLE)`)
fmt.Fprintln(writer, ` set(DEFAULT_COMPILE_FLAGS "${DEFAULT_COMPILE_FLAGS_MACOS}")`)
fmt.Fprintln(writer, `else()`)
fmt.Fprintln(writer, ` set(DEFAULT_COMPILE_FLAGS "${DEFAULT_COMPILE_FLAGS_LINUX}")`)
fmt.Fprintln(writer, `endif()`)
}
// Return the copts rule attribute for the given rule.
func getRuleCopts(r *build.Rule) ([]string, error) {
ruleOpts, err := getRuleStringArrayAttribute(r, "copts")
if err != nil {
return nil, skerr.Wrap(err)
}
copts := []string{"${DEFAULT_COMPILE_FLAGS}"}
return appendUnique(copts, ruleOpts...), nil
}
// Return the include paths for the supplied rule and all rules on which
// this rule depends.
//
// Note: All rules are absolute paths.
func getRuleIncludes(r *build.Rule, qr *analysis_v2.CqueryResult) ([]string, error) {
deps, err := getRuleStringArrayAttribute(r, "deps")
if err != nil {
return nil, skerr.Wrap(err)
}
includes, err := getRuleStringArrayAttribute(r, "includes")
if err != nil {
return nil, skerr.Wrap(err)
}
ruleDir, err := getLocationDir(r.GetLocation())
if err != nil {
return nil, skerr.Wrap(err)
}
for idx, inc := range includes {
if inc == "." {
includes[idx] = ruleDir
}
}
for _, d := range deps {
dr := findRule(qr, d)
if dr == nil {
return nil, skerr.Fmt("cannot find rule %s", d)
}
if isExternalRule(dr.GetName()) {
continue
}
incs, err := getRuleIncludes(dr, qr)
if err != nil {
return nil, skerr.Wrap(err)
}
includes = appendUnique(includes, incs...)
}
return includes, nil
}
// Return the deps for the supplied rule and all rules on which
// this rule depends.
func getRuleDefines(r *build.Rule, qr *analysis_v2.CqueryResult) ([]string, error) {
deps, err := getRuleStringArrayAttribute(r, "deps")
if err != nil {
return nil, skerr.Wrap(err)
}
defines, err := getRuleStringArrayAttribute(r, "defines")
if err != nil {
return nil, skerr.Wrap(err)
}
for _, d := range deps {
dr := findRule(qr, d)
if dr == nil {
return nil, skerr.Fmt("cannot find rule %s", d)
}
defs, err := getRuleDefines(dr, qr)
if err != nil {
return nil, skerr.Wrap(err)
}
defines = appendUnique(defines, defs...)
}
return defines, nil
}
// Convert an absolute path to a file *within the workspace* to a
// workspace relative path. All paths start with ${CMAKE_SOURCE_DIR}.
func (e *CMakeExporter) absToWorkspaceRelativePath(absPath string) string {
if absPath == e.workspaceDir {
return "${CMAKE_SOURCE_DIR}"
}
return fmt.Sprintf("${CMAKE_SOURCE_DIR}/%s", absPath[len(e.workspaceDir)+1:])
}
// Write the list of items (which may be rules or files) to the supplied buffer.
func (e *CMakeExporter) writeItems(r *cmakeRule, projectDir string, items []string, buffer *bytes.Buffer) error {
for _, item := range items {
if isFileTarget(item) {
_, _, target, err := parseRule(item)
if err != nil {
return skerr.Wrap(err)
}
absPath := filepath.Join(projectDir, target)
fmt.Fprintf(buffer, " %q\n", e.absToWorkspaceRelativePath(absPath))
} else {
cmakeName, err := getRuleSimpleName(item)
if err != nil {
return skerr.Wrap(err)
}
fmt.Fprintf(buffer, " ${%s}\n", cmakeName)
err = r.addDependency(item)
if err != nil {
return skerr.Wrap(err)
}
}
}
return nil
}
// Write the "srcs" and "hdrs" rule attributes to the supplied buffer.
func (e *CMakeExporter) writeSrcsAndHdrs(rule *cmakeRule, buffer *bytes.Buffer, r *build.Rule) error {
ruleDir, err := getLocationDir(r.GetLocation())
if err != nil {
return skerr.Wrap(err)
}
for _, attrib := range r.Attribute {
if attrib.GetName() == "srcs" {
if attrib.GetType() != build.Attribute_LABEL_LIST {
return skerr.Fmt(`srcs in rule %q is not a list`, r.GetName())
}
fmt.Fprintln(buffer, " # Sources:")
err := e.writeItems(rule, ruleDir, attrib.GetStringListValue(), buffer)
if err != nil {
return skerr.Wrap(err)
}
}
if attrib.GetName() == "hdrs" {
if attrib.GetType() != build.Attribute_LABEL_LIST {
return skerr.Fmt(`hdrs in rule %q is not a list`, r.GetName())
}
fmt.Fprintln(buffer, " # Headers:")
err := e.writeItems(rule, ruleDir, attrib.GetStringListValue(), buffer)
if err != nil {
return skerr.Wrap(err)
}
}
}
return nil
}
// Write the target COMPILE_FLAGS property to the supplied buffer (if there are any copts).
func (e *CMakeExporter) writeCompileFlags(r *build.Rule, buffer *bytes.Buffer) error {
copts, err := getRuleCopts(r)
if err != nil {
return skerr.Wrap(err)
}
if len(copts) == 0 {
// No error, just nothing to write.
return nil
}
str := strings.Join(copts, " ")
cmakeName, err := getRuleSimpleName(r.GetName())
if err != nil {
return skerr.Wrap(err)
}
_, err = fmt.Fprintf(buffer, "set_target_properties(%s PROPERTIES COMPILE_FLAGS\n %q\n)\n",
cmakeName, str)
return err
}
// Write the target COMPILE_DEFINITIONS property to the supplied buffer (if there are any defines).
func (e *CMakeExporter) writeCompileDefinitions(r *build.Rule, qr *analysis_v2.CqueryResult, buffer *bytes.Buffer) error {
defines, err := getRuleDefines(r, qr)
if err != nil {
return skerr.Wrap(err)
}
if len(defines) == 0 {
// No error, just nothing to write.
return nil
}
str := strings.Join(defines, ";")
cmakeName, err := getRuleSimpleName(r.GetName())
if err != nil {
return skerr.Wrap(err)
}
_, err = fmt.Fprintf(buffer, "set_target_properties(%s PROPERTIES COMPILE_DEFINITIONS\n %q\n)\n", cmakeName, str)
return err
}
// Write the target INCLUDE_DIRECTORIES property to the supplied buffer (if there are any).
func (e *CMakeExporter) writeIncludeDirectories(r *build.Rule, qr *analysis_v2.CqueryResult, buffer *bytes.Buffer) error {
includes, err := getRuleIncludes(r, qr)
if err != nil {
return skerr.Wrap(err)
}
includes = appendUnique(includes, e.workspaceDir)
for i, path := range includes {
includes[i] = e.absToWorkspaceRelativePath(path)
}
str := strings.Join(includes, ";")
cmakeName, err := getRuleSimpleName(r.GetName())
if err != nil {
return skerr.Wrap(err)
}
_, err = fmt.Fprintf(buffer, "set_target_properties(%s PROPERTIES INCLUDE_DIRECTORIES\n %q\n)\n", cmakeName, str)
return err
}
// Write the target LINK_FLAGS property to the supplied buffer (if there are any linkopts).
func (e *CMakeExporter) writeLinkFlags(r *build.Rule, buffer *bytes.Buffer) error {
defines, err := getRuleStringArrayAttribute(r, "linkopts")
if err != nil {
return skerr.Wrap(err)
}
if len(defines) == 0 {
// No error, just nothing to write.
return nil
}
str := strings.Join(defines, " ")
cmakeName, err := getRuleSimpleName(r.GetName())
if err != nil {
return skerr.Wrap(err)
}
_, err = fmt.Fprintf(buffer, "set_target_properties(%s PROPERTIES LINK_FLAGS\n %q\n)\n", cmakeName, str)
return err
}
// Write all target properties to the supplied buffer.
func (e *CMakeExporter) writeProperties(r *build.Rule, qr *analysis_v2.CqueryResult, buffer *bytes.Buffer) error {
err := e.writeCompileFlags(r, buffer)
if err != nil {
return skerr.Wrap(err)
}
err = e.writeLinkFlags(r, buffer)
if err != nil {
return skerr.Wrap(err)
}
err = e.writeCompileDefinitions(r, qr, buffer)
if err != nil {
return skerr.Wrap(err)
}
err = e.writeIncludeDirectories(r, qr, buffer)
if err != nil {
return skerr.Wrap(err)
}
return nil
}
// Convert the filegroup rule to the CMake equivalent.
func (e *CMakeExporter) convertFilegroupRule(r *build.Rule) error {
rule := e.workspace.createRule(r)
var contents bytes.Buffer
targetName := r.GetName()
variableName, err := getRuleSimpleName(r.GetName())
if err != nil {
return skerr.Wrap(err)
}
fmt.Fprintf(&contents, "# %s\n", targetName)
fmt.Fprintf(&contents, "list(APPEND %s\n", variableName)
err = e.writeSrcsAndHdrs(rule, &contents, r)
if err != nil {
return skerr.Wrap(err)
}
fmt.Fprintln(&contents, ")")
rule.setContents(contents.Bytes())
return nil
}
// Convert the cc_binary rule to the CMake equivalent.
func (e *CMakeExporter) convertCCBinaryRule(r *build.Rule, qr *analysis_v2.CqueryResult) error {
rule := e.workspace.createRule(r)
targetName := r.GetName()
var contents bytes.Buffer
fmt.Fprintf(&contents, "# %s\n", targetName)
cmakeName, err := getRuleSimpleName(r.GetName())
if err != nil {
return skerr.Wrap(err)
}
fmt.Fprintf(&contents, "add_executable(%s \"\")\n", cmakeName)
fmt.Fprintf(&contents, "target_sources(%s\n", cmakeName)
fmt.Fprintln(&contents, " PRIVATE")
err = e.writeSrcsAndHdrs(rule, &contents, r)
if err != nil {
return skerr.Wrap(err)
}
fmt.Fprintln(&contents, ")")
err = e.writeProperties(r, qr, &contents)
if err != nil {
return skerr.Wrap(err)
}
rule.setContents(contents.Bytes())
return nil
}
// Convert the cc_library rule to the CMake equivalent.
func (e *CMakeExporter) convertCCLibraryRule(r *build.Rule, qr *analysis_v2.CqueryResult) error {
rule := e.workspace.createRule(r)
targetName := r.GetName()
cmakeName, err := getRuleSimpleName(r.GetName())
if err != nil {
return skerr.Wrap(err)
}
var contents bytes.Buffer
fmt.Fprintf(&contents, "# %s\n", targetName)
fmt.Fprintf(&contents, "add_library(%s \"\")\n", cmakeName)
fmt.Fprintf(&contents, "target_sources(%s\n", cmakeName)
fmt.Fprintln(&contents, " PRIVATE")
err = e.writeSrcsAndHdrs(rule, &contents, r)
if err != nil {
return skerr.Wrap(err)
}
fmt.Fprintln(&contents, ")")
err = e.writeProperties(r, qr, &contents)
if err != nil {
return skerr.Wrap(err)
}
rule.setContents(contents.Bytes())
return nil
}
// Export will convert the input Bazel cquery output, provided by the
// supplied QueryCommand parameter, to CMake. The equivalent
// CMake project definition will be written using the writer provided
// to the constructor method.
func (e *CMakeExporter) Export(qcmd interfaces.QueryCommand) error {
in, err := qcmd.Read()
if err != nil {
return skerr.Wrapf(err, "error reading Bazel cquery data")
}
qr := analysis_v2.CqueryResult{}
if err := proto.Unmarshal(in, &qr); err != nil {
return skerr.Wrapf(err, "failed to unmarshal Bazel cquery result")
}
writer, err := e.fs.OpenFile(e.cmakeFile)
if err != nil {
return skerr.Wrap(err)
}
fmt.Fprintln(writer, "# DO NOT EDIT: This file is auto-generated.")
fmt.Fprintln(writer, "cmake_minimum_required(VERSION 3.13)")
writer.WriteString("\n")
fmt.Fprintf(writer, "project(%s LANGUAGES C CXX)\n", e.projName)
writer.WriteString("\n")
writePlatformCompileFlags(writer)
writer.WriteString("\n")
for _, result := range qr.GetResults() {
t := result.GetTarget()
r := t.GetRule()
if isExternalRule(r.GetName()) {
continue
}
var err error = nil
switch {
case r.GetRuleClass() == "cc_binary":
err = e.convertCCBinaryRule(r, &qr)
case r.GetRuleClass() == "cc_library":
err = e.convertCCLibraryRule(r, &qr)
case r.GetRuleClass() == "filegroup":
err = e.convertFilegroupRule(r)
}
if err != nil {
return skerr.Wrapf(err, "failed to convert %s", r.GetRuleClass())
}
}
_, err = e.workspace.write(writer)
if err != nil {
return skerr.Wrap(err)
}
return nil
}
// Make sure CMakeExporter fulfills the Exporter interface.
var _ interfaces.Exporter = (*CMakeExporter)(nil)