1// Copyright 2017 Google Inc. All rights reserved.
2//
3// Licensed under the Apache License, Version 2.0 (the "License");
4// you may not use this file except in compliance with the License.
5// You may obtain a copy of the License at
6//
7//     http://www.apache.org/licenses/LICENSE-2.0
8//
9// Unless required by applicable law or agreed to in writing, software
10// distributed under the License is distributed on an "AS IS" BASIS,
11// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12// See the License for the specific language governing permissions and
13// limitations under the License.
14
15package cc
16
17import (
18	"fmt"
19
20	"android/soong/android"
21	"os"
22	"path"
23	"path/filepath"
24	"strings"
25)
26
27// This singleton generates CMakeLists.txt files. It does so for each blueprint Android.bp resulting in a cc.Module
28// when either make, mm, mma, mmm or mmma is called. CMakeLists.txt files are generated in a separate folder
29// structure (see variable CLionOutputProjectsDirectory for root).
30
31func init() {
32	android.RegisterSingletonType("cmakelists_generator", cMakeListsGeneratorSingleton)
33}
34
35func cMakeListsGeneratorSingleton() android.Singleton {
36	return &cmakelistsGeneratorSingleton{}
37}
38
39type cmakelistsGeneratorSingleton struct{}
40
41const (
42	cMakeListsFilename              = "CMakeLists.txt"
43	cLionAggregateProjectsDirectory = "development" + string(os.PathSeparator) + "ide" + string(os.PathSeparator) + "clion"
44	cLionOutputProjectsDirectory    = "out" + string(os.PathSeparator) + cLionAggregateProjectsDirectory
45	minimumCMakeVersionSupported    = "3.5"
46
47	// Environment variables used to modify behavior of this singleton.
48	envVariableGenerateCMakeLists = "SOONG_GEN_CMAKEFILES"
49	envVariableGenerateDebugInfo  = "SOONG_GEN_CMAKEFILES_DEBUG"
50	envVariableTrue               = "1"
51)
52
53// Instruct generator to trace how header include path and flags were generated.
54// This is done to ease investigating bug reports.
55var outputDebugInfo = false
56
57func (c *cmakelistsGeneratorSingleton) GenerateBuildActions(ctx android.SingletonContext) {
58	if getEnvVariable(envVariableGenerateCMakeLists, ctx) != envVariableTrue {
59		return
60	}
61
62	outputDebugInfo = (getEnvVariable(envVariableGenerateDebugInfo, ctx) == envVariableTrue)
63
64	// Track which projects have already had CMakeLists.txt generated to keep the first
65	// variant for each project.
66	seenProjects := map[string]bool{}
67
68	ctx.VisitAllModules(func(module android.Module) {
69		if ccModule, ok := module.(*Module); ok {
70			if compiledModule, ok := ccModule.compiler.(CompiledInterface); ok {
71				generateCLionProject(compiledModule, ctx, ccModule, seenProjects)
72			}
73		}
74	})
75
76	// Link all handmade CMakeLists.txt aggregate from
77	//     BASE/development/ide/clion to
78	// BASE/out/development/ide/clion.
79	dir := filepath.Join(android.AbsSrcDirForExistingUseCases(), cLionAggregateProjectsDirectory)
80	filepath.Walk(dir, linkAggregateCMakeListsFiles)
81
82	return
83}
84
85func getEnvVariable(name string, ctx android.SingletonContext) string {
86	// Using android.Config.Getenv instead of os.getEnv to guarantee soong will
87	// re-run in case this environment variable changes.
88	return ctx.Config().Getenv(name)
89}
90
91func exists(path string) bool {
92	_, err := os.Stat(path)
93	if err == nil {
94		return true
95	}
96	if os.IsNotExist(err) {
97		return false
98	}
99	return true
100}
101
102func linkAggregateCMakeListsFiles(path string, info os.FileInfo, err error) error {
103
104	if info == nil {
105		return nil
106	}
107
108	dst := strings.Replace(path, cLionAggregateProjectsDirectory, cLionOutputProjectsDirectory, 1)
109	if info.IsDir() {
110		// This is a directory to create
111		os.MkdirAll(dst, os.ModePerm)
112	} else {
113		// This is a file to link
114		os.Remove(dst)
115		os.Symlink(path, dst)
116	}
117	return nil
118}
119
120func generateCLionProject(compiledModule CompiledInterface, ctx android.SingletonContext, ccModule *Module,
121	seenProjects map[string]bool) {
122	srcs := compiledModule.Srcs()
123	if len(srcs) == 0 {
124		return
125	}
126
127	// Only write CMakeLists.txt for the first variant of each architecture of each module
128	clionprojectLocation := getCMakeListsForModule(ccModule, ctx)
129	if seenProjects[clionprojectLocation] {
130		return
131	}
132
133	seenProjects[clionprojectLocation] = true
134
135	// Ensure the directory hosting the cmakelists.txt exists
136	projectDir := path.Dir(clionprojectLocation)
137	os.MkdirAll(projectDir, os.ModePerm)
138
139	// Create cmakelists.txt
140	f, _ := os.Create(filepath.Join(projectDir, cMakeListsFilename))
141	defer f.Close()
142
143	// Header.
144	f.WriteString("# THIS FILE WAS AUTOMATICALY GENERATED!\n")
145	f.WriteString("# ANY MODIFICATION WILL BE OVERWRITTEN!\n\n")
146	f.WriteString("# To improve project view in Clion    :\n")
147	f.WriteString("# Tools > CMake > Change Project Root  \n\n")
148	f.WriteString(fmt.Sprintf("cmake_minimum_required(VERSION %s)\n", minimumCMakeVersionSupported))
149	f.WriteString(fmt.Sprintf("project(%s)\n", ccModule.ModuleBase.Name()))
150	f.WriteString(fmt.Sprintf("set(ANDROID_ROOT %s)\n\n", android.AbsSrcDirForExistingUseCases()))
151
152	pathToCC, _ := evalVariable(ctx, "${config.ClangBin}/")
153	f.WriteString(fmt.Sprintf("set(CMAKE_C_COMPILER \"%s%s\")\n", buildCMakePath(pathToCC), "clang"))
154	f.WriteString(fmt.Sprintf("set(CMAKE_CXX_COMPILER \"%s%s\")\n", buildCMakePath(pathToCC), "clang++"))
155
156	// Add all sources to the project.
157	f.WriteString("list(APPEND\n")
158	f.WriteString("     SOURCE_FILES\n")
159	for _, src := range srcs {
160		f.WriteString(fmt.Sprintf("    ${ANDROID_ROOT}/%s\n", src.String()))
161	}
162	f.WriteString(")\n")
163
164	// Add all header search path and compiler parameters (-D, -W, -f, -XXXX)
165	f.WriteString("\n# GLOBAL ALL FLAGS:\n")
166	globalAllParameters := parseCompilerParameters(ccModule.flags.Global.CommonFlags, ctx, f)
167	translateToCMake(globalAllParameters, f, true, true)
168
169	f.WriteString("\n# LOCAL ALL FLAGS:\n")
170	localAllParameters := parseCompilerParameters(ccModule.flags.Local.CommonFlags, ctx, f)
171	translateToCMake(localAllParameters, f, true, true)
172
173	f.WriteString("\n# GLOBAL CFLAGS:\n")
174	globalCParameters := parseCompilerParameters(ccModule.flags.Global.CFlags, ctx, f)
175	translateToCMake(globalCParameters, f, true, true)
176
177	f.WriteString("\n# LOCAL CFLAGS:\n")
178	localCParameters := parseCompilerParameters(ccModule.flags.Local.CFlags, ctx, f)
179	translateToCMake(localCParameters, f, true, true)
180
181	f.WriteString("\n# GLOBAL C ONLY FLAGS:\n")
182	globalConlyParameters := parseCompilerParameters(ccModule.flags.Global.ConlyFlags, ctx, f)
183	translateToCMake(globalConlyParameters, f, true, false)
184
185	f.WriteString("\n# LOCAL C ONLY FLAGS:\n")
186	localConlyParameters := parseCompilerParameters(ccModule.flags.Local.ConlyFlags, ctx, f)
187	translateToCMake(localConlyParameters, f, true, false)
188
189	f.WriteString("\n# GLOBAL CPP FLAGS:\n")
190	globalCppParameters := parseCompilerParameters(ccModule.flags.Global.CppFlags, ctx, f)
191	translateToCMake(globalCppParameters, f, false, true)
192
193	f.WriteString("\n# LOCAL CPP FLAGS:\n")
194	localCppParameters := parseCompilerParameters(ccModule.flags.Local.CppFlags, ctx, f)
195	translateToCMake(localCppParameters, f, false, true)
196
197	f.WriteString("\n# GLOBAL SYSTEM INCLUDE FLAGS:\n")
198	globalIncludeParameters := parseCompilerParameters(ccModule.flags.SystemIncludeFlags, ctx, f)
199	translateToCMake(globalIncludeParameters, f, true, true)
200
201	// Add project executable.
202	f.WriteString(fmt.Sprintf("\nadd_executable(%s ${SOURCE_FILES})\n",
203		cleanExecutableName(ccModule.ModuleBase.Name())))
204}
205
206func cleanExecutableName(s string) string {
207	return strings.Replace(s, "@", "-", -1)
208}
209
210func translateToCMake(c compilerParameters, f *os.File, cflags bool, cppflags bool) {
211	writeAllIncludeDirectories(c.systemHeaderSearchPath, f, true)
212	writeAllIncludeDirectories(c.headerSearchPath, f, false)
213	if cflags {
214		writeAllRelativeFilePathFlags(c.relativeFilePathFlags, f, "CMAKE_C_FLAGS")
215		writeAllFlags(c.flags, f, "CMAKE_C_FLAGS")
216	}
217	if cppflags {
218		writeAllRelativeFilePathFlags(c.relativeFilePathFlags, f, "CMAKE_CXX_FLAGS")
219		writeAllFlags(c.flags, f, "CMAKE_CXX_FLAGS")
220	}
221	if c.sysroot != "" {
222		f.WriteString(fmt.Sprintf("include_directories(SYSTEM \"%s\")\n", buildCMakePath(path.Join(c.sysroot, "usr", "include"))))
223	}
224
225}
226
227func buildCMakePath(p string) string {
228	if path.IsAbs(p) {
229		return p
230	}
231	return fmt.Sprintf("${ANDROID_ROOT}/%s", p)
232}
233
234func writeAllIncludeDirectories(includes []string, f *os.File, isSystem bool) {
235	if len(includes) == 0 {
236		return
237	}
238
239	system := ""
240	if isSystem {
241		system = "SYSTEM"
242	}
243
244	f.WriteString(fmt.Sprintf("include_directories(%s \n", system))
245
246	for _, include := range includes {
247		f.WriteString(fmt.Sprintf("    \"%s\"\n", buildCMakePath(include)))
248	}
249	f.WriteString(")\n\n")
250
251	// Also add all headers to source files.
252	f.WriteString("file (GLOB_RECURSE TMP_HEADERS\n")
253	for _, include := range includes {
254		f.WriteString(fmt.Sprintf("    \"%s/**/*.h\"\n", buildCMakePath(include)))
255	}
256	f.WriteString(")\n")
257	f.WriteString("list (APPEND SOURCE_FILES ${TMP_HEADERS})\n\n")
258}
259
260type relativeFilePathFlagType struct {
261	flag             string
262	relativeFilePath string
263}
264
265func writeAllRelativeFilePathFlags(relativeFilePathFlags []relativeFilePathFlagType, f *os.File, tag string) {
266	for _, flag := range relativeFilePathFlags {
267		f.WriteString(fmt.Sprintf("set(%s \"${%s} %s=%s\")\n", tag, tag, flag.flag, buildCMakePath(flag.relativeFilePath)))
268	}
269}
270
271func writeAllFlags(flags []string, f *os.File, tag string) {
272	for _, flag := range flags {
273		f.WriteString(fmt.Sprintf("set(%s \"${%s} %s\")\n", tag, tag, flag))
274	}
275}
276
277type parameterType int
278
279const (
280	headerSearchPath parameterType = iota
281	variable
282	systemHeaderSearchPath
283	flag
284	systemRoot
285	relativeFilePathFlag
286)
287
288type compilerParameters struct {
289	headerSearchPath       []string
290	systemHeaderSearchPath []string
291	flags                  []string
292	sysroot                string
293	// Must be in a=b/c/d format and can be split into "a" and "b/c/d"
294	relativeFilePathFlags []relativeFilePathFlagType
295}
296
297func makeCompilerParameters() compilerParameters {
298	return compilerParameters{
299		sysroot: "",
300	}
301}
302
303func categorizeParameter(parameter string) parameterType {
304	if strings.HasPrefix(parameter, "-I") {
305		return headerSearchPath
306	}
307	if strings.HasPrefix(parameter, "$") {
308		return variable
309	}
310	if strings.HasPrefix(parameter, "-isystem") {
311		return systemHeaderSearchPath
312	}
313	if strings.HasPrefix(parameter, "-isysroot") {
314		return systemRoot
315	}
316	if strings.HasPrefix(parameter, "--sysroot") {
317		return systemRoot
318	}
319	if strings.HasPrefix(parameter, "-fsanitize-blacklist") {
320		return relativeFilePathFlag
321	}
322	if strings.HasPrefix(parameter, "-fprofile-sample-use") {
323		return relativeFilePathFlag
324	}
325	return flag
326}
327
328// Flattens a list of strings potentially containing space characters into a list of string containing no
329// spaces.
330func normalizeParameters(params []string) []string {
331	var flatParams []string
332	for _, s := range params {
333		s = strings.Trim(s, " ")
334		if len(s) == 0 {
335			continue
336		}
337		flatParams = append(flatParams, strings.Split(s, " ")...)
338	}
339	return flatParams
340}
341
342func parseCompilerParameters(params []string, ctx android.SingletonContext, f *os.File) compilerParameters {
343	var compilerParameters = makeCompilerParameters()
344
345	for i, str := range params {
346		f.WriteString(fmt.Sprintf("# Raw param [%d] = '%s'\n", i, str))
347	}
348
349	// Soong does not guarantee that each flag will be in an individual string. e.g: The
350	// input received could be:
351	// params = {"-isystem", "path/to/system"}
352	// or it could be
353	// params = {"-isystem path/to/system"}
354	// To normalize the input, we split all strings with the "space" character and consolidate
355	// all tokens into a flattened parameters list
356	params = normalizeParameters(params)
357
358	for i := 0; i < len(params); i++ {
359		param := params[i]
360		if param == "" {
361			continue
362		}
363
364		switch categorizeParameter(param) {
365		case headerSearchPath:
366			compilerParameters.headerSearchPath =
367				append(compilerParameters.headerSearchPath, strings.TrimPrefix(param, "-I"))
368		case variable:
369			if evaluated, error := evalVariable(ctx, param); error == nil {
370				if outputDebugInfo {
371					f.WriteString(fmt.Sprintf("# variable %s = '%s'\n", param, evaluated))
372				}
373
374				paramsFromVar := parseCompilerParameters(strings.Split(evaluated, " "), ctx, f)
375				concatenateParams(&compilerParameters, paramsFromVar)
376
377			} else {
378				if outputDebugInfo {
379					f.WriteString(fmt.Sprintf("# variable %s could NOT BE RESOLVED\n", param))
380				}
381			}
382		case systemHeaderSearchPath:
383			if i < len(params)-1 {
384				compilerParameters.systemHeaderSearchPath =
385					append(compilerParameters.systemHeaderSearchPath, params[i+1])
386			} else if outputDebugInfo {
387				f.WriteString("# Found a header search path marker with no path")
388			}
389			i = i + 1
390		case flag:
391			c := cleanupParameter(param)
392			f.WriteString(fmt.Sprintf("# FLAG '%s' became %s\n", param, c))
393			compilerParameters.flags = append(compilerParameters.flags, c)
394		case systemRoot:
395			if i < len(params)-1 {
396				compilerParameters.sysroot = params[i+1]
397			} else if outputDebugInfo {
398				f.WriteString("# Found a system root path marker with no path")
399			}
400			i = i + 1
401		case relativeFilePathFlag:
402			flagComponents := strings.Split(param, "=")
403			if len(flagComponents) == 2 {
404				flagStruct := relativeFilePathFlagType{flag: flagComponents[0], relativeFilePath: flagComponents[1]}
405				compilerParameters.relativeFilePathFlags = append(compilerParameters.relativeFilePathFlags, flagStruct)
406			} else {
407				if outputDebugInfo {
408					f.WriteString(fmt.Sprintf("# Relative File Path Flag [%s] is not formatted as a=b/c/d \n", param))
409				}
410			}
411		}
412	}
413	return compilerParameters
414}
415
416func cleanupParameter(p string) string {
417	// In the blueprint, c flags can be passed as:
418	//  cflags: [ "-DLOG_TAG=\"libEGL\"", ]
419	// which becomes:
420	// '-DLOG_TAG="libEGL"' in soong.
421	// In order to be injected in CMakelists.txt we need to:
422	// - Remove the wrapping ' character
423	// - Double escape all special \ and " characters.
424	// For a end result like:
425	// -DLOG_TAG=\\\"libEGL\\\"
426	if !strings.HasPrefix(p, "'") || !strings.HasSuffix(p, "'") || len(p) < 3 {
427		return p
428	}
429
430	// Reverse wrapper quotes and escaping that may have happened in NinjaAndShellEscape
431	// TODO:  It is ok to reverse here for now but if NinjaAndShellEscape becomes more complex,
432	// we should create a method NinjaAndShellUnescape in escape.go and use that instead.
433	p = p[1 : len(p)-1]
434	p = strings.Replace(p, `'\''`, `'`, -1)
435	p = strings.Replace(p, `$$`, `$`, -1)
436
437	p = doubleEscape(p)
438	return p
439}
440
441func escape(s string) string {
442	s = strings.Replace(s, `\`, `\\`, -1)
443	s = strings.Replace(s, `"`, `\"`, -1)
444	return s
445}
446
447func doubleEscape(s string) string {
448	s = escape(s)
449	s = escape(s)
450	return s
451}
452
453func concatenateParams(c1 *compilerParameters, c2 compilerParameters) {
454	c1.headerSearchPath = append(c1.headerSearchPath, c2.headerSearchPath...)
455	c1.systemHeaderSearchPath = append(c1.systemHeaderSearchPath, c2.systemHeaderSearchPath...)
456	if c2.sysroot != "" {
457		c1.sysroot = c2.sysroot
458	}
459	c1.flags = append(c1.flags, c2.flags...)
460}
461
462func evalVariable(ctx android.SingletonContext, str string) (string, error) {
463	evaluated, err := ctx.Eval(pctx, str)
464	if err == nil {
465		return evaluated, nil
466	}
467	return "", err
468}
469
470func getCMakeListsForModule(module *Module, ctx android.SingletonContext) string {
471	return filepath.Join(android.AbsSrcDirForExistingUseCases(),
472		cLionOutputProjectsDirectory,
473		path.Dir(ctx.BlueprintFile(module)),
474		module.ModuleBase.Name()+"-"+
475			module.ModuleBase.Arch().ArchType.Name+"-"+
476			module.ModuleBase.Os().Name,
477		cMakeListsFilename)
478}
479