diff --git a/builder.go b/builder.go index d5b8eff7..bcb57c9e 100644 --- a/builder.go +++ b/builder.go @@ -128,6 +128,8 @@ func (s *Builder) Run(ctx *types.Context) error { &PrintUsedLibrariesIfVerbose{}, + &ExportProjectCMake{SketchError: mainErr != nil}, + &phases.Sizer{SketchError: mainErr != nil}, } otherErr := runCommands(ctx, commands, false) diff --git a/constants/constants.go b/constants/constants.go index d83ee273..cca3ceaf 100644 --- a/constants/constants.go +++ b/constants/constants.go @@ -48,10 +48,11 @@ const BUILD_PROPERTIES_BUILD_SYSTEM_PATH = "build.system.path" const BUILD_PROPERTIES_BUILD_VARIANT = "build.variant" const BUILD_PROPERTIES_BUILD_VARIANT_PATH = "build.variant.path" const BUILD_PROPERTIES_COMPILER_C_ELF_FLAGS = "compiler.c.elf.flags" -const BUILD_PROPERTIES_COMPILER_C_ELF_EXTRAFLAGS = "compiler.c.elf.extra_flags" +const BUILD_PROPERTIES_COMPILER_LDFLAGS = "compiler.ldflags" const BUILD_PROPERTIES_COMPILER_CPP_FLAGS = "compiler.cpp.flags" const BUILD_PROPERTIES_COMPILER_PATH = "compiler.path" const BUILD_PROPERTIES_COMPILER_WARNING_FLAGS = "compiler.warning_flags" +const BUILD_PROPERTIES_COMPILER_EXPORT_CMAKE_FLAGS = "compiler.export_cmake" const BUILD_PROPERTIES_EXTRA_TIME_DST = "extra.time.dst" const BUILD_PROPERTIES_EXTRA_TIME_LOCAL = "extra.time.local" const BUILD_PROPERTIES_EXTRA_TIME_UTC = "extra.time.utc" diff --git a/create_cmake_rule.go b/create_cmake_rule.go new file mode 100644 index 00000000..085efb91 --- /dev/null +++ b/create_cmake_rule.go @@ -0,0 +1,227 @@ +/* + * This file is part of Arduino Builder. + * + * Arduino Builder is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA + * + * As a special exception, you may use this file as part of a free software + * library without restriction. Specifically, if other files instantiate + * templates or use macros or inline functions from this file, or you compile + * this file and link it with other files to produce an executable, this + * file does not by itself cause the resulting executable to be covered by + * the GNU General Public License. This exception does not however + * invalidate any other reasons why the executable file might be covered by + * the GNU General Public License. + * + * Copyright 2015 Arduino LLC (http://www.arduino.cc/) + */ + +package builder + +import ( + "fmt" + "os" + "path/filepath" + "strings" + + "github.com/arduino/arduino-builder/builder_utils" + "github.com/arduino/arduino-builder/constants" + "github.com/arduino/arduino-builder/i18n" + "github.com/arduino/arduino-builder/types" + "github.com/arduino/arduino-builder/utils" +) + +var VALID_EXPORT_EXTENSIONS = map[string]bool{".h": true, ".c": true, ".hpp": true, ".hh": true, ".cpp": true, ".s": true, ".a": true} +var DOTHEXTENSION = map[string]bool{".h": true, ".hh": true, ".hpp": true} +var DOTAEXTENSION = map[string]bool{".a": true} + +type ExportProjectCMake struct { + // Was there an error while compiling the sketch? + SketchError bool +} + +func (s *ExportProjectCMake) Run(ctx *types.Context) error { + //verbose := ctx.Verbose + logger := ctx.GetLogger() + + if s.SketchError || !canExportCmakeProject(ctx) { + return nil + } + + // Create new cmake subFolder - clean if the folder is already there + cmakeFolder := filepath.Join(ctx.BuildPath, "_cmake") + if _, err := os.Stat(cmakeFolder); err == nil { + os.RemoveAll(cmakeFolder) + } + os.Mkdir(cmakeFolder, 0777) + + // Create lib and build subfolders + libBaseFolder := filepath.Join(cmakeFolder, "lib") + os.Mkdir(libBaseFolder, 0777) + buildBaseFolder := filepath.Join(cmakeFolder, "build") + os.Mkdir(buildBaseFolder, 0777) + + // Create core subfolder path (don't create it yet) + coreFolder := filepath.Join(cmakeFolder, "core") + cmakeFile := filepath.Join(cmakeFolder, "CMakeLists.txt") + + // Copy used libraries in the correct folder + extensions := func(ext string) bool { return VALID_EXPORT_EXTENSIONS[ext] } + for _, library := range ctx.ImportedLibraries { + libFolder := filepath.Join(libBaseFolder, library.Name) + utils.CopyDir(library.Folder, libFolder, extensions) + // Remove examples folder + if _, err := os.Stat(filepath.Join(libFolder, "examples")); err == nil { + os.RemoveAll(filepath.Join(libFolder, "examples")) + } + } + + // Copy core + variant in use + preprocessed sketch in the correct folders + err := utils.CopyDir(ctx.BuildProperties[constants.BUILD_PROPERTIES_BUILD_CORE_PATH], coreFolder, extensions) + if err != nil { + fmt.Println(err) + } + err = utils.CopyDir(ctx.BuildProperties[constants.BUILD_PROPERTIES_BUILD_VARIANT_PATH], filepath.Join(coreFolder, "variant"), extensions) + if err != nil { + fmt.Println(err) + } + err = utils.CopyDir(ctx.SketchBuildPath, filepath.Join(cmakeFolder, "sketch"), extensions) + if err != nil { + fmt.Println(err) + } + + // Extract CFLAGS, CPPFLAGS and LDFLAGS + var defines []string + var linkerflags []string + var libs []string + var linkDirectories []string + + extractCompileFlags(ctx, constants.RECIPE_C_COMBINE_PATTERN, &defines, &libs, &linkerflags, &linkDirectories, logger) + extractCompileFlags(ctx, constants.RECIPE_C_PATTERN, &defines, &libs, &linkerflags, &linkDirectories, logger) + extractCompileFlags(ctx, constants.RECIPE_CPP_PATTERN, &defines, &libs, &linkerflags, &linkDirectories, logger) + + // Extract folders with .h in them for adding in include list + var headerFiles []string + isHeader := func(ext string) bool { return DOTHEXTENSION[ext] } + utils.FindFilesInFolder(&headerFiles, cmakeFolder, isHeader, true) + foldersContainingDotH := findUniqueFoldersRelative(headerFiles, cmakeFolder) + + // Extract folders with .a in them for adding in static libs paths list + var staticLibsFiles []string + isStaticLib := func(ext string) bool { return DOTAEXTENSION[ext] } + utils.FindFilesInFolder(&staticLibsFiles, cmakeFolder, isStaticLib, true) + + // Generate the CMakeLists global file + + projectName := strings.TrimSuffix(filepath.Base(ctx.Sketch.MainFile.Name), filepath.Ext(ctx.Sketch.MainFile.Name)) + + cmakelist := "cmake_minimum_required(VERSION 3.5.0)\n" + cmakelist += "INCLUDE(FindPkgConfig)\n" + cmakelist += "project (" + projectName + " C CXX)\n" + cmakelist += "add_definitions (" + strings.Join(defines, " ") + " " + strings.Join(linkerflags, " ") + ")\n" + cmakelist += "include_directories (" + foldersContainingDotH + ")\n" + + // Make link directories relative + // We can totally discard them since they mostly are outside the core folder + // If they are inside the core they are not getting copied :) + var relLinkDirectories []string + for _, dir := range linkDirectories { + if strings.Contains(dir, cmakeFolder) { + relLinkDirectories = append(relLinkDirectories, strings.TrimPrefix(dir, cmakeFolder)) + } + } + + // Add SO_PATHS option for libraries not getting found by pkg_config + cmakelist += "set(EXTRA_LIBS_DIRS \"\" CACHE STRING \"Additional paths for dynamic libraries\")\n" + + for i, lib := range libs { + // Dynamic libraries should be discovered by pkg_config + lib = strings.TrimPrefix(lib, "-l") + libs[i] = lib + cmakelist += "pkg_search_module (" + strings.ToUpper(lib) + " " + lib + ")\n" + relLinkDirectories = append(relLinkDirectories, "${"+strings.ToUpper(lib)+"_LIBRARY_DIRS}") + } + cmakelist += "link_directories (" + strings.Join(relLinkDirectories, " ") + " ${EXTRA_LIBS_DIRS})\n" + for _, staticLibsFile := range staticLibsFiles { + // Static libraries are fully configured + lib := filepath.Base(staticLibsFile) + lib = strings.TrimPrefix(lib, "lib") + lib = strings.TrimSuffix(lib, ".a") + if !utils.SliceContains(libs, lib) { + libs = append(libs, lib) + cmakelist += "add_library (" + lib + " STATIC IMPORTED)\n" + location := strings.TrimPrefix(staticLibsFile, cmakeFolder) + cmakelist += "set_property(TARGET " + lib + " PROPERTY IMPORTED_LOCATION " + "${PROJECT_SOURCE_DIR}" + location + " )\n" + } + } + // Include source files + // TODO: remove .cpp and .h from libraries example folders + cmakelist += "file (GLOB_RECURSE SOURCES core/*.c* lib/*.c* sketch/*.c*)\n" + + // Compile and link project + cmakelist += "add_executable (" + projectName + " ${SOURCES} ${SOURCES_LIBS})\n" + cmakelist += "target_link_libraries( " + projectName + " " + strings.Join(libs, " ") + ")\n" + + utils.WriteFile(cmakeFile, cmakelist) + + return nil +} + +func canExportCmakeProject(ctx *types.Context) bool { + return ctx.BuildProperties[constants.BUILD_PROPERTIES_COMPILER_EXPORT_CMAKE_FLAGS] != "" +} + +func extractCompileFlags(ctx *types.Context, receipe string, defines, libs, linkerflags, linkDirectories *[]string, logger i18n.Logger) { + command, _ := builder_utils.PrepareCommandForRecipe(ctx.BuildProperties, receipe, true, false, false, logger) + + for _, arg := range command.Args { + if strings.HasPrefix(arg, "-D") { + *defines = appendIfUnique(*defines, arg) + continue + } + if strings.HasPrefix(arg, "-l") { + *libs = appendIfUnique(*libs, arg) + continue + } + if strings.HasPrefix(arg, "-L") { + *linkDirectories = appendIfUnique(*linkDirectories, strings.TrimPrefix(arg, "-L")) + continue + } + if strings.HasPrefix(arg, "-") && !strings.HasPrefix(arg, "-I") { + // HACK : from linkerflags remove MMD (no cache is produced) + if !strings.HasPrefix(arg, "-MMD") { + *linkerflags = appendIfUnique(*linkerflags, arg) + } + } + } +} + +func findUniqueFoldersRelative(slice []string, base string) string { + var out []string + for _, element := range slice { + path := filepath.Dir(element) + path = strings.TrimPrefix(path, base+"/") + if !utils.SliceContains(out, path) { + out = append(out, path) + } + } + return strings.Join(out, " ") +} + +func appendIfUnique(slice []string, element string) []string { + if !utils.SliceContains(slice, element) { + slice = append(slice, element) + } + return slice +} diff --git a/phases/libraries_builder.go b/phases/libraries_builder.go index fda56b80..d80fcf91 100644 --- a/phases/libraries_builder.go +++ b/phases/libraries_builder.go @@ -93,7 +93,7 @@ func fixLDFLAGforPrecompiledLibraries(ctx *types.Context, libraries []*types.Lib name = strings.Replace(name, "lib", "", 1) libs_cmd += "-l" + name + " " } - ctx.BuildProperties[constants.BUILD_PROPERTIES_COMPILER_C_ELF_EXTRAFLAGS] += "\"-L" + path + "\" " + libs_cmd + ctx.BuildProperties[constants.BUILD_PROPERTIES_COMPILER_LDFLAGS] += "\"-L" + path + "\" " + libs_cmd } } return nil diff --git a/utils/utils.go b/utils/utils.go index 318b3984..a5682c29 100644 --- a/utils/utils.go +++ b/utils/utils.go @@ -32,6 +32,8 @@ package utils import ( "crypto/md5" "encoding/hex" + "fmt" + "io" "io/ioutil" "os" "os/exec" @@ -488,3 +490,109 @@ func ParseCppString(line string) (string, string, bool) { i += width } } + +// CopyFile copies the contents of the file named src to the file named +// by dst. The file will be created if it does not already exist. If the +// destination file exists, all it's contents will be replaced by the contents +// of the source file. The file mode will be copied from the source and +// the copied data is synced/flushed to stable storage. +func CopyFile(src, dst string) (err error) { + in, err := os.Open(src) + if err != nil { + return + } + defer in.Close() + + out, err := os.Create(dst) + if err != nil { + return + } + defer func() { + if e := out.Close(); e != nil { + err = e + } + }() + + _, err = io.Copy(out, in) + if err != nil { + return + } + + err = out.Sync() + if err != nil { + return + } + + si, err := os.Stat(src) + if err != nil { + return + } + err = os.Chmod(dst, si.Mode()) + if err != nil { + return + } + + return +} + +// CopyDir recursively copies a directory tree, attempting to preserve permissions. +// Source directory must exist, destination directory must *not* exist. +// Symlinks are ignored and skipped. +func CopyDir(src string, dst string, extensions CheckExtensionFunc) (err error) { + src = filepath.Clean(src) + dst = filepath.Clean(dst) + + si, err := os.Stat(src) + if err != nil { + return err + } + if !si.IsDir() { + return fmt.Errorf("source is not a directory") + } + + _, err = os.Stat(dst) + if err != nil && !os.IsNotExist(err) { + return + } + if err == nil { + return fmt.Errorf("destination already exists") + } + + err = os.MkdirAll(dst, si.Mode()) + if err != nil { + return + } + + entries, err := ioutil.ReadDir(src) + if err != nil { + return + } + + for _, entry := range entries { + srcPath := filepath.Join(src, entry.Name()) + dstPath := filepath.Join(dst, entry.Name()) + + if entry.IsDir() { + err = CopyDir(srcPath, dstPath, extensions) + if err != nil { + return + } + } else { + // Skip symlinks. + if entry.Mode()&os.ModeSymlink != 0 { + continue + } + + if extensions != nil && !extensions(strings.ToLower(filepath.Ext(srcPath))) { + continue + } + + err = CopyFile(srcPath, dstPath) + if err != nil { + return + } + } + } + + return +}