• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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