// 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)