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