1// Copyright 2022 Google LLC 2// 3// Use of this source code is governed by a BSD-style license that can be 4// found in the LICENSE file. 5 6package exporter 7 8import ( 9 "bytes" 10 "fmt" 11 "path/filepath" 12 "strings" 13 14 "go.skia.org/infra/go/skerr" 15 "go.skia.org/skia/bazel/exporter/build_proto/analysis_v2" 16 "go.skia.org/skia/bazel/exporter/build_proto/build" 17 "go.skia.org/skia/bazel/exporter/interfaces" 18 "google.golang.org/protobuf/proto" 19) 20 21type CMakeExporter struct { 22 projName string 23 workspace cmakeWorkspace 24 workspaceDir string // Absolute path to Bazel workspace directory. 25 cmakeFile string // Absolute path to CMake output file. 26 fs interfaces.FileSystem 27} 28 29// NewCMakeExporter creates an exporter that will export a Bazel project 30// query from a project in the workspaceDir to a CMake project file identified 31// by cmakeFile. 32// 33// Note: cmakeFile must be an absolute path. 34func NewCMakeExporter(projName, workspaceDir, cmakeFile string, fs interfaces.FileSystem) *CMakeExporter { 35 return &CMakeExporter{ 36 workspace: *newCMakeWorkspace(), 37 projName: projName, 38 workspaceDir: workspaceDir, 39 cmakeFile: cmakeFile, 40 fs: fs, 41 } 42} 43 44// Return the default copts (COMPILE_FLAGS in CMake) for the macOS toolchain. 45func getMacPlatformRuleCopts() []string { 46 // TODO(crbug.com/skia/13586): Retrieve these values from Bazel. 47 // These values must match those values defined in mac_toolchain_config.bzl 48 return []string{ 49 // These items are from _make_default_flags(). 50 "-std=c++17", 51 "-Wno-psabi", 52 53 // From _make_target_specific_flags. 54 "--target=arm64-apple-macos11", 55 } 56} 57 58// Return the default copts (COMPILE_FLAGS in CMake) for the Linux toolchain. 59func getLinuxPlatformRuleCopts() []string { 60 // TODO(crbug.com/skia/13586): Retrieve these values from Bazel. 61 return []string{ 62 // These items are from _make_default_flags(). 63 "-std=c++17", 64 "-Wno-psabi", 65 66 // Added to avoid compile warning. 67 "-Wno-attributes", 68 } 69} 70 71// Write the CMake project config to set the COMPILE_FLAGS 72// variables for all platforms. 73func writePlatformCompileFlags(writer interfaces.Writer) { 74 val := strings.Join(getMacPlatformRuleCopts(), " ") 75 fmt.Fprintf(writer, "set(DEFAULT_COMPILE_FLAGS_MACOS %q)\n", val) 76 77 val = strings.Join(getLinuxPlatformRuleCopts(), " ") 78 fmt.Fprintf(writer, "set(DEFAULT_COMPILE_FLAGS_LINUX %q)\n", val) 79 writer.WriteString("\n") 80 fmt.Fprintln(writer, `if (APPLE)`) 81 fmt.Fprintln(writer, ` set(DEFAULT_COMPILE_FLAGS "${DEFAULT_COMPILE_FLAGS_MACOS}")`) 82 fmt.Fprintln(writer, `else()`) 83 fmt.Fprintln(writer, ` set(DEFAULT_COMPILE_FLAGS "${DEFAULT_COMPILE_FLAGS_LINUX}")`) 84 fmt.Fprintln(writer, `endif()`) 85} 86 87// Return the copts rule attribute for the given rule. 88func getRuleCopts(r *build.Rule) ([]string, error) { 89 ruleOpts, err := getRuleStringArrayAttribute(r, "copts") 90 if err != nil { 91 return nil, skerr.Wrap(err) 92 } 93 copts := []string{"${DEFAULT_COMPILE_FLAGS}"} 94 return appendUnique(copts, ruleOpts...), nil 95} 96 97// Return the include paths for the supplied rule and all rules on which 98// this rule depends. 99// 100// Note: All rules are absolute paths. 101func getRuleIncludes(r *build.Rule, qr *analysis_v2.CqueryResult) ([]string, error) { 102 deps, err := getRuleStringArrayAttribute(r, "deps") 103 if err != nil { 104 return nil, skerr.Wrap(err) 105 } 106 includes, err := getRuleStringArrayAttribute(r, "includes") 107 if err != nil { 108 return nil, skerr.Wrap(err) 109 } 110 ruleDir, err := getLocationDir(r.GetLocation()) 111 if err != nil { 112 return nil, skerr.Wrap(err) 113 } 114 for idx, inc := range includes { 115 if inc == "." { 116 includes[idx] = ruleDir 117 } 118 } 119 for _, d := range deps { 120 dr := findRule(qr, d) 121 if dr == nil { 122 return nil, skerr.Fmt("cannot find rule %s", d) 123 } 124 if isExternalRule(dr.GetName()) { 125 continue 126 } 127 incs, err := getRuleIncludes(dr, qr) 128 if err != nil { 129 return nil, skerr.Wrap(err) 130 } 131 includes = appendUnique(includes, incs...) 132 } 133 return includes, nil 134} 135 136// Return the deps for the supplied rule and all rules on which 137// this rule depends. 138func getRuleDefines(r *build.Rule, qr *analysis_v2.CqueryResult) ([]string, error) { 139 deps, err := getRuleStringArrayAttribute(r, "deps") 140 if err != nil { 141 return nil, skerr.Wrap(err) 142 } 143 defines, err := getRuleStringArrayAttribute(r, "defines") 144 if err != nil { 145 return nil, skerr.Wrap(err) 146 } 147 for _, d := range deps { 148 dr := findRule(qr, d) 149 if dr == nil { 150 return nil, skerr.Fmt("cannot find rule %s", d) 151 } 152 defs, err := getRuleDefines(dr, qr) 153 if err != nil { 154 return nil, skerr.Wrap(err) 155 } 156 defines = appendUnique(defines, defs...) 157 } 158 return defines, nil 159} 160 161// Convert an absolute path to a file *within the workspace* to a 162// workspace relative path. All paths start with ${CMAKE_SOURCE_DIR}. 163func (e *CMakeExporter) absToWorkspaceRelativePath(absPath string) string { 164 if absPath == e.workspaceDir { 165 return "${CMAKE_SOURCE_DIR}" 166 } 167 return fmt.Sprintf("${CMAKE_SOURCE_DIR}/%s", absPath[len(e.workspaceDir)+1:]) 168} 169 170// Write the list of items (which may be rules or files) to the supplied buffer. 171func (e *CMakeExporter) writeItems(r *cmakeRule, projectDir string, items []string, buffer *bytes.Buffer) error { 172 for _, item := range items { 173 if isFileTarget(item) { 174 _, _, target, err := parseRule(item) 175 if err != nil { 176 return skerr.Wrap(err) 177 } 178 absPath := filepath.Join(projectDir, target) 179 fmt.Fprintf(buffer, " %q\n", e.absToWorkspaceRelativePath(absPath)) 180 } else { 181 cmakeName, err := getRuleSimpleName(item) 182 if err != nil { 183 return skerr.Wrap(err) 184 } 185 fmt.Fprintf(buffer, " ${%s}\n", cmakeName) 186 err = r.addDependency(item) 187 if err != nil { 188 return skerr.Wrap(err) 189 } 190 } 191 } 192 return nil 193} 194 195// Write the "srcs" and "hdrs" rule attributes to the supplied buffer. 196func (e *CMakeExporter) writeSrcsAndHdrs(rule *cmakeRule, buffer *bytes.Buffer, r *build.Rule) error { 197 ruleDir, err := getLocationDir(r.GetLocation()) 198 if err != nil { 199 return skerr.Wrap(err) 200 } 201 for _, attrib := range r.Attribute { 202 if attrib.GetName() == "srcs" { 203 if attrib.GetType() != build.Attribute_LABEL_LIST { 204 return skerr.Fmt(`srcs in rule %q is not a list`, r.GetName()) 205 } 206 fmt.Fprintln(buffer, " # Sources:") 207 err := e.writeItems(rule, ruleDir, attrib.GetStringListValue(), buffer) 208 if err != nil { 209 return skerr.Wrap(err) 210 } 211 } 212 if attrib.GetName() == "hdrs" { 213 if attrib.GetType() != build.Attribute_LABEL_LIST { 214 return skerr.Fmt(`hdrs in rule %q is not a list`, r.GetName()) 215 } 216 fmt.Fprintln(buffer, " # Headers:") 217 err := e.writeItems(rule, ruleDir, attrib.GetStringListValue(), buffer) 218 if err != nil { 219 return skerr.Wrap(err) 220 } 221 } 222 } 223 return nil 224} 225 226// Write the target COMPILE_FLAGS property to the supplied buffer (if there are any copts). 227func (e *CMakeExporter) writeCompileFlags(r *build.Rule, buffer *bytes.Buffer) error { 228 copts, err := getRuleCopts(r) 229 if err != nil { 230 return skerr.Wrap(err) 231 } 232 if len(copts) == 0 { 233 // No error, just nothing to write. 234 return nil 235 } 236 str := strings.Join(copts, " ") 237 cmakeName, err := getRuleSimpleName(r.GetName()) 238 if err != nil { 239 return skerr.Wrap(err) 240 } 241 _, err = fmt.Fprintf(buffer, "set_target_properties(%s PROPERTIES COMPILE_FLAGS\n %q\n)\n", 242 cmakeName, str) 243 return err 244} 245 246// Write the target COMPILE_DEFINITIONS property to the supplied buffer (if there are any defines). 247func (e *CMakeExporter) writeCompileDefinitions(r *build.Rule, qr *analysis_v2.CqueryResult, buffer *bytes.Buffer) error { 248 defines, err := getRuleDefines(r, qr) 249 if err != nil { 250 return skerr.Wrap(err) 251 } 252 if len(defines) == 0 { 253 // No error, just nothing to write. 254 return nil 255 } 256 str := strings.Join(defines, ";") 257 cmakeName, err := getRuleSimpleName(r.GetName()) 258 if err != nil { 259 return skerr.Wrap(err) 260 } 261 _, err = fmt.Fprintf(buffer, "set_target_properties(%s PROPERTIES COMPILE_DEFINITIONS\n %q\n)\n", cmakeName, str) 262 return err 263} 264 265// Write the target INCLUDE_DIRECTORIES property to the supplied buffer (if there are any). 266func (e *CMakeExporter) writeIncludeDirectories(r *build.Rule, qr *analysis_v2.CqueryResult, buffer *bytes.Buffer) error { 267 includes, err := getRuleIncludes(r, qr) 268 if err != nil { 269 return skerr.Wrap(err) 270 } 271 includes = appendUnique(includes, e.workspaceDir) 272 for i, path := range includes { 273 includes[i] = e.absToWorkspaceRelativePath(path) 274 } 275 str := strings.Join(includes, ";") 276 cmakeName, err := getRuleSimpleName(r.GetName()) 277 if err != nil { 278 return skerr.Wrap(err) 279 } 280 _, err = fmt.Fprintf(buffer, "set_target_properties(%s PROPERTIES INCLUDE_DIRECTORIES\n %q\n)\n", cmakeName, str) 281 return err 282} 283 284// Write the target LINK_FLAGS property to the supplied buffer (if there are any linkopts). 285func (e *CMakeExporter) writeLinkFlags(r *build.Rule, buffer *bytes.Buffer) error { 286 defines, err := getRuleStringArrayAttribute(r, "linkopts") 287 if err != nil { 288 return skerr.Wrap(err) 289 } 290 if len(defines) == 0 { 291 // No error, just nothing to write. 292 return nil 293 } 294 str := strings.Join(defines, " ") 295 cmakeName, err := getRuleSimpleName(r.GetName()) 296 if err != nil { 297 return skerr.Wrap(err) 298 } 299 _, err = fmt.Fprintf(buffer, "set_target_properties(%s PROPERTIES LINK_FLAGS\n %q\n)\n", cmakeName, str) 300 return err 301} 302 303// Write all target properties to the supplied buffer. 304func (e *CMakeExporter) writeProperties(r *build.Rule, qr *analysis_v2.CqueryResult, buffer *bytes.Buffer) error { 305 err := e.writeCompileFlags(r, buffer) 306 if err != nil { 307 return skerr.Wrap(err) 308 } 309 err = e.writeLinkFlags(r, buffer) 310 if err != nil { 311 return skerr.Wrap(err) 312 } 313 err = e.writeCompileDefinitions(r, qr, buffer) 314 if err != nil { 315 return skerr.Wrap(err) 316 } 317 err = e.writeIncludeDirectories(r, qr, buffer) 318 if err != nil { 319 return skerr.Wrap(err) 320 } 321 return nil 322} 323 324// Convert the filegroup rule to the CMake equivalent. 325func (e *CMakeExporter) convertFilegroupRule(r *build.Rule) error { 326 327 rule := e.workspace.createRule(r) 328 329 var contents bytes.Buffer 330 331 targetName := r.GetName() 332 variableName, err := getRuleSimpleName(r.GetName()) 333 if err != nil { 334 return skerr.Wrap(err) 335 } 336 fmt.Fprintf(&contents, "# %s\n", targetName) 337 fmt.Fprintf(&contents, "list(APPEND %s\n", variableName) 338 339 err = e.writeSrcsAndHdrs(rule, &contents, r) 340 if err != nil { 341 return skerr.Wrap(err) 342 } 343 fmt.Fprintln(&contents, ")") 344 rule.setContents(contents.Bytes()) 345 346 return nil 347} 348 349// Convert the cc_binary rule to the CMake equivalent. 350func (e *CMakeExporter) convertCCBinaryRule(r *build.Rule, qr *analysis_v2.CqueryResult) error { 351 352 rule := e.workspace.createRule(r) 353 354 targetName := r.GetName() 355 var contents bytes.Buffer 356 fmt.Fprintf(&contents, "# %s\n", targetName) 357 cmakeName, err := getRuleSimpleName(r.GetName()) 358 if err != nil { 359 return skerr.Wrap(err) 360 } 361 fmt.Fprintf(&contents, "add_executable(%s \"\")\n", cmakeName) 362 fmt.Fprintf(&contents, "target_sources(%s\n", cmakeName) 363 fmt.Fprintln(&contents, " PRIVATE") 364 365 err = e.writeSrcsAndHdrs(rule, &contents, r) 366 if err != nil { 367 return skerr.Wrap(err) 368 } 369 370 fmt.Fprintln(&contents, ")") 371 err = e.writeProperties(r, qr, &contents) 372 if err != nil { 373 return skerr.Wrap(err) 374 } 375 rule.setContents(contents.Bytes()) 376 377 return nil 378} 379 380// Convert the cc_library rule to the CMake equivalent. 381func (e *CMakeExporter) convertCCLibraryRule(r *build.Rule, qr *analysis_v2.CqueryResult) error { 382 383 rule := e.workspace.createRule(r) 384 385 targetName := r.GetName() 386 cmakeName, err := getRuleSimpleName(r.GetName()) 387 if err != nil { 388 return skerr.Wrap(err) 389 } 390 var contents bytes.Buffer 391 fmt.Fprintf(&contents, "# %s\n", targetName) 392 fmt.Fprintf(&contents, "add_library(%s \"\")\n", cmakeName) 393 fmt.Fprintf(&contents, "target_sources(%s\n", cmakeName) 394 fmt.Fprintln(&contents, " PRIVATE") 395 396 err = e.writeSrcsAndHdrs(rule, &contents, r) 397 if err != nil { 398 return skerr.Wrap(err) 399 } 400 fmt.Fprintln(&contents, ")") 401 err = e.writeProperties(r, qr, &contents) 402 if err != nil { 403 return skerr.Wrap(err) 404 } 405 406 rule.setContents(contents.Bytes()) 407 408 return nil 409} 410 411// Export will convert the input Bazel cquery output, provided by the 412// supplied QueryCommand parameter, to CMake. The equivalent 413// CMake project definition will be written using the writer provided 414// to the constructor method. 415func (e *CMakeExporter) Export(qcmd interfaces.QueryCommand) error { 416 417 in, err := qcmd.Read() 418 if err != nil { 419 return skerr.Wrapf(err, "error reading Bazel cquery data") 420 } 421 qr := analysis_v2.CqueryResult{} 422 if err := proto.Unmarshal(in, &qr); err != nil { 423 return skerr.Wrapf(err, "failed to unmarshal Bazel cquery result") 424 } 425 426 writer, err := e.fs.OpenFile(e.cmakeFile) 427 if err != nil { 428 return skerr.Wrap(err) 429 } 430 fmt.Fprintln(writer, "# DO NOT EDIT: This file is auto-generated.") 431 fmt.Fprintln(writer, "cmake_minimum_required(VERSION 3.13)") 432 writer.WriteString("\n") 433 fmt.Fprintf(writer, "project(%s LANGUAGES C CXX)\n", e.projName) 434 writer.WriteString("\n") 435 436 writePlatformCompileFlags(writer) 437 writer.WriteString("\n") 438 439 for _, result := range qr.GetResults() { 440 t := result.GetTarget() 441 r := t.GetRule() 442 if isExternalRule(r.GetName()) { 443 continue 444 } 445 var err error = nil 446 switch { 447 case r.GetRuleClass() == "cc_binary": 448 err = e.convertCCBinaryRule(r, &qr) 449 case r.GetRuleClass() == "cc_library": 450 err = e.convertCCLibraryRule(r, &qr) 451 case r.GetRuleClass() == "filegroup": 452 err = e.convertFilegroupRule(r) 453 } 454 if err != nil { 455 return skerr.Wrapf(err, "failed to convert %s", r.GetRuleClass()) 456 } 457 } 458 459 _, err = e.workspace.write(writer) 460 if err != nil { 461 return skerr.Wrap(err) 462 } 463 464 return nil 465} 466 467// Make sure CMakeExporter fulfills the Exporter interface. 468var _ interfaces.Exporter = (*CMakeExporter)(nil) 469